ANDPADのモバイル技術採用方針について

はじめに

こんにちはCDOの山下です。

ANDPADでは現在5種類のアプリをリリースしています。 リリース時期もストアのversion 1.0.0が登録された時期から大まかに推定するとこんな感じでした。 建築という業界であってもiOSの利用者数は多いため、そちらを先行して開発する傾向にありました。

アプリ名 役割 iOSリリース時期 Androidリリース時期 技術
ANDPAD 施工管理 2016/3 2016/3 swift/kotlin フルネイティブ
ANDPAD CHAT チャット 2016/5 2017/3 swift/kotlin フルネイティブ
ANDPAD 図面 図面を管理・共有 2018/3 - swift フルネイティブ
ADNPAD 検査 検査業務に特化 2016/8 2018/2 react native -> flutterに移行予定
ANDPAD 短工事 短期工事のための施工管理 2019/2 2019/4 flutter

私たちが提供しているANDPADというサービスは、2016年3月のサービスリリース以降、全国の職人の方々が毎日の業務の中で非常に頻繁にアクセスして使っていただけるサービスになりました。

頻繁に使ってもらえるという部分では特にモバイルアプリは大きな貢献をしています。 データの観点からもモバイルアプリとWebの利用者数を比較してみると2倍以上の差があります。

想定している利用シーンから考えても、現場の職人さんは外で業務を行うためPCからではなく、スマホアプリから情報の確認・登録などの アクションを行なっています。また、建築現場においてもスマホ端末は比較的多くのユーザーが所持していますが、PCはなかなか全てのユーザーが 所持するツールではありません。ですので全ての人に使ってもらうには難しい面もあります。

モバイルアプリの開発方針

より早く市場に新しい良いプロダクトを投入するために

サービスリリース当初は、私たちはユーザーの情報共有・コミュニケーション課題を解決するために尽力しており、「施工管理」「チャット」アプリを注力して開発していました。今も注力して開発していることに変わりありませんが、「建築業界自体がIT化が進んでいないこと」と「巨大な市場であること」からプロダクトとして求められる課題解決が多方面に渡ることがわかってきました。

そのような理由から、1つのアプリで課題を解決するのではなく複数のアプリで課題解決していく方針を取っています。このような方針は単一のアプリで勝負していく事業形態とは大きく異なります。

開発視点では、複数のアプリをリリースすることが前提でどのように複数のアプリを磨き込んで作っていけるかが大きな課題となります。 日々の開発に埋もれてしまうとその場の戦いになりがちですが、アプリチームでは現在以下の3つの方針で開発を進めていこうという話をしています。

1. iOS/Androidのコードの共通化 - Flutterの採用

リスト表示やグリッド表示などのアプリでよく使われるようなUIで構成されるアプリは、Flutterを採用して開発する方針にしています。

このような標準的なUIに関してはユーザー体験の面でもネイティブと遜色ないパフォーマンスを発揮できるため、クロスプラットフォームのFlutterを積極的に活用したいと思っています。現在新規サービスの短工事のみFlutterで開発していますが、検査アプリもFlutterに置き換えています。また、既存のネイティブアプリにおいても標準的なUIで作られるような機能に関しては年末に正式リリースされたadd-to-appの機能を活用していこうとしています。 flutter.dev

今のアプリチームでは、ネイティブアプリの開発とFlutterでの開発が両方できる必要がありますが、毎朝Flutter勉強会を開催しており、元々ネイティブアプリ専任で入社したメンバーもFlutter開発ができるような状態になってきました。 Flutterは、アーキテクチャの流行り廃りが激しいので既存コードが陳腐化しやすくて大変ですが、エンジニアとしてはその辺りも楽しんでやっています。

2. 共通機能のモジュール化 - 基盤機能の拡充

アプリの中でも特にユーザー体験として重要で、より磨き込んでいくと決めた機能をアプリにおける「ANDPAD基盤」として定義しています。

モジュール化および共通化し、より独立して機能を磨き込んで開発できるようにしています。 複数のアプリを作っているとは言っても、高度にモジュール化を進めていければたまたまドメインの境界がアプリの境界となっているだけで、ANDPADというサービス全体で捉えると単一のアプリを磨き込んでいくのと開発アプローチは大きく変わらないのではないかと捉えています。

f:id:oct88:20200608221252p:plain
複数のアプリをANDPAD基盤を拡充させながら作っていく

ソフトウェア的にはモジュール化することで、複雑なUI操作が入り込んでしまったソースコードなどを既存コードから分離でき、エンハンスできる環境を作れるので良いこともあります。

しかし、人数が少ない中で切り出すと改善のサイクルが回しにくいというデメリットもあります。開発できる環境はあっても工数の問題で改善が後回しになりがちでもあります。直近数十人規模でアプリ開発メンバーも増える計画ですので、人が増えれば解決していく想定をしています。

3. テストコードの拡充

「良いプロダクト」を作るために、我々ができることは品質を上げることでもありますので、テストコード比率を上げていく取り組みもしています。

モバイルアプリのテストは、サービスリリース当初は1年以内にiOS/Android × 施工管理/チャットの計4個のアプリを最速でリリースすることが重要であったため正直、重要視していませんでした。 最初の取り組みとしては、 テストピラミッドの上部のUIテストから導入していきました。

f:id:oct88:20200712162543p:plain
テストピラミッド

テストの基礎  |  Android デベロッパー  |  Android Developersdeveloper.android.com

現在は「共通機能の切り出し+テストコードの追加」と「リファクタ」によりUnit Testの比率を上げていこうとしています。また、アプリチーム全員がiOS/Androidのテストコードの理解を深める目的で週1で勉強会を行っています。全員が専門領域にとらわれずに全プラットフォームのテストコードを書いている状態を目標にしています。

まとめ

最初1人から始まったANDPADのアプリのリリースから、直近1年間でアプリチームの社員数は4人になりました。

組織スケールは「自分の仕事を誰か他の人に渡す」というポリシーで動いているので、今は実際のアプリ開発業務からは離れつつあります(それだけ優秀なメンバーが集まりつつあります)。

私たちの事業は、業界に特化していますがドメイン領域が狭いわけではありません。

建築業界には様々な業務が存在していてIT化が進んでいないことで非効率になっている業務は沢山存在しています。モバイルアプリのエンジニアとしては、自分が開発した1アプリで業界で働く人たちの働き方を一変させるインパクトがあります。

自分の作ったアプリで世の中の建築業界を変えていく、そんな仕事を一緒にしてくれるアプリエンジニアの方を積極募集しています。 是非ご興味ありましたら採用サイトもご覧ください。

engineer.andpad.co.jp

ANDPADが向き合う建設業界とは

はじめに

株式会社アンドパッドVPoPの山口です。
以前こういったインタビュー記事も出させて頂きました。

eng.kandc.com

さて、プロダクトマネージャーとして半年前に入社し、建設テック業界シェアNo.1サービスである「ANDPAD」のプロダクト企画および運営を担っています。

以前の職場では不動産情報サービスを担当していたこともあり、建設業界に比較的近い立場で仕事をしていました。実際に入社し、関わってみると予想を遥かに超える深さ、やりがいを感じて日々頑張っております。

まだ新参者ではありますが、自分の振り返りも含めて建設業界×ITのスケール、やりがいについて現在の視点で書いていこうと思います。よろしければお付き合いください。

建設業界の市場規模

image.itmedia.co.jp
政府統計によると、2018年間の出来高*総計は54兆6,057億円であると発表がされています。内訳は民間工事が33兆2,569億円、公共工事21兆3,488億円です。(”出来高”は建設に係るコスト総計(人件費や材料費など)を指します)

仮に流通取扱高(GMV)の20%シェアを獲得することができれば、約10兆円となり国内主要ECサービスの合計を上回ります。いかに市場規模が大きいかがわかりますね。

日本を支える中心国策事業ですので規模感的には想像ができるのですが、これだけ大きな事業であるにも関わらず、多くの方々が想像されている通り、IT化による効率化が最も遅れている業界と言われています。

私なりの想像ですが、理由は

1.業界特有の業務性質、難易度
2.就労されている方の老齢化

1と2は関連性が強く、このことにより様ざまな効率化着手が遅れてきたのだと考えています。


f:id:fukadume7:20200706094953j:plain

業界特有の業務性質、難易度

現場で働く職人の方々の労苦については、いまさら言及するまでもないので、ここではあえて割愛します。

地元の町を歩いているとマンションの建設現場に出くわすのですが、暑い日も寒い日も一生懸命に取り組まれていて、ただただ頭が下がる思いです。

一方、現場監督はどうなのでしょうか。

いつも拝読させていただいている「施工の神様」の記事によると、監督はまっさらな建設予定土地を調査している段階で「工事中の現場がどうなる」「実際に建設がされたらどうなる」を予想するべし、とあります。

sekokan-navi.jp


まさにテクノロジーの塊で、培ってきた人間のカンの凄さを思わせるのですが、この事実によって「監督の仕事は難しい、大変」というイメージを拡げているのかもしれません。実際、自分に「やって」と言われたら数か月眠れない自信があります(笑)

就労されている方の老齢化

関連して、難易度が高く属人的になりやすい業界だからこそ、より新しい方の参入障壁が高まっていくのはある意味自然なことだといえます。

監督の方々も忙しいため、新人教育にふんだんに時間をかけるわけにもいかないですし、この時代に「背中を観て学べ」と突き放すのも、さすがに無理がありすぎます。

こうして若手の方々の就労意欲が低下し続けることで、現場で働く人々の数が減り、一方で建設需要は伸び続け……ますます建設現場は大変になってきているというのが、今の現状の理解です。

残業が増える一方で、見合う収入に跳ね返っているかと言うと、おそらく課題があるというのが一般的な認識です。

ANDPADのミッション

そうした業界をIT化によって、属人的なテクノロジーを一般化し、効率化しようとしているのがANDPADです。

「施工管理」=「プロジェクト管理」という前知識で仕事に就いたわけですが、予実管理だけでここまで複雑なロジックがあるのかと、入社早々に面食らいました……。

自分はプロダクトマネージャー的な仕事について10年以上キャリアもあり、プロジェクト管理等、業務ツールを企画開発することに対して、そこまでの壁があるとは思っていなかったので、なかなかに衝撃的でもありました。

一方で、ここまで複雑なことを人的努力だけで積み重ね、ノウハウを作ってきた業界に本気で興味をもつこともできました。

お客様の元に出向いてご意見を聞かせていただく機会があるのですが、ANDPADもお客様によって使い方にいろんな工夫がなされ「えっ、そうやって使っているのですか?!」と驚くこともしばしば。(不足機能を運用でカバーいただいている事なので、本来よくないのですが)

働く方々の知恵の結晶が業界を支えているのだと、つくづく思います。

ちょっとおっかないけど懐が深くて、実はものすごくいろんなことを複雑に考える必要があって、考えがいがある。まだ入社して間もないですが、この業界に貢献することができて、本当に良かったと思っています。

さいごに

ANDPADでは、「幸せを築く人を、幸せに。」をミッションに建築業界のみなさまを支援すべく様ざまなサービスを提供しています。
もしご興味をもたれた方、一緒に課題解決に取り組んでみませんか?

engineer.andpad.co.jp

MySQL勉強会〜ロックについて〜を開催しました!

f:id:fkmy:20200713122808j:plain

はじめまして、ソフトウェアエンジニアの福間( fkmy )です。早いもので入社してから10ヶ月が経ちました。 普段は新規サービスのAPIRailsで書いてます。リモートワークも数ヶ月目に入り自宅の開発環境が快適になってきました。先月はモニターアームを買いましたが出費がかさみますね。

さて、この記事は7/6(月)にANDPADのデータベースの技術顧問をして頂いてる三谷(@mita2)さんに発表してもらった開発部向けのMySQL勉強会(ロック基礎編)のレポートとなります。 今回は在宅勤務期間中のためオンライン開催となり、当日は16名が参加していました。


開催背景

社内からデータベースのロックについて勉強会をしたい!!というメンバーからの声があり開催となりました。

※ブログで取り上げられてはいないのですが「MySQLのチューニング」についても1ヶ月前に実施しています。


内容

当日の資料はこちらになります。


個人的に気になった内容や質問をいかで5つピックアップして以下で紹介したいと思います。

1. なぜロックが必要か

f:id:fkmy:20200707161846p:plain

  • パフォーマンスを出すためにトランザクションを並列で実行するとレコード競合が発生してしまうため、競合を防ぐためにロックが必要となります。
  • 余談ですが実装依存で発生するロックとは別に内部で実行されるシステム的なロックはラッチ(Latch)と言うそうです。


2. InnoDBのロック

f:id:fkmy:20200707162046p:plain 「条件に合致した行」に対するロックではなく「インデックスに対するロック」が掛かること、ロックの範囲は実行計画に依存することを事例を踏まえて紹介していただきました。また大量DELETE時はロックが発生するケースが多いため、対象レコードの主キーをSELECTして削除するのがお作法だそうです。


3. 外部キーのロック

f:id:fkmy:20200707163013p:plain 外部キーを持つレコードを追加/更新する場合、親テーブルにも共有ロックが掛かります。 親テーブルにロックが掛かる理由ですが外部キー制約違反を防ぐためとなります。


4. ロックに関するエラーの調査方法

f:id:fkmy:20200707163447p:plain 競合しているトランザクションの調査方法、デッドロックの対処方法について具体例を元にログのみるべきポイント、推測方法などを紹介していただきました。


5. 「大量データのインポート処理を All or Nothingでしたい場合、ロックの問題は発生するのか?」

外部キーを持つ子テーブルの場合、親テーブルに対して共有ロックが発生する可能性があります。 All or Nothingにはなりませんが、子テーブルへのインポート処理のトランザクションを細かくする方法も考えられます。


さいごに

普段あまりロックのことまで気にして開発することがなく、InnoDBや外部キーのロックについても知らない状態だったので勉強になりました。ロックのエラー調査については経験による推測や訓練が必要になるとのことなので私もMySQLの気持ちを推測できるように経験を積んでいきたいと思います。

データベース勉強会は月1くらいの頻度で開催されているので次回の開催も楽しみです!


ANDPADではエンジニアを募集しています

今回のMySQL勉強会に限らず定期的に勉強会を開催してみんなで成長しようとしていますので、もしご興味持っていただけたら以下の採用サイトから詳しい職場環境などもご覧になってみてください!

engineer.andpad.co.jp

OpenIDConnect+Deviseでの認証クライアントの実装

ソフトウェアエンジニアの彌冨です。
github.com

入社してからもうすぐで2年になろうとしています。
ベンチャーあるあるでいろいろとエンジニア領域外なこともやってきましたが、最近新規サービスをフルスクラッチで作り上げている中で苦労したユーザー認証の話を書きます。

 

前置き

OpenID Connectとは

こちらでは実装の話に集中するため、詳細の話は以下のスライドがわかりやすいので参考にしてください(OpenID Connect自体の話はある程度割愛させていただければと思います)

www.slideshare.net


ただ、端的に私の理解を述べると、OAuth2.0のプロトコルを拡張してシンプルなアイデンティティレイヤーを足すことで、認証と認可の両方を行うことができ、よりセキュアにアクセス管理ができる規格と考えています。


今回は以下のOpenID ConnectのAuthorization Code Flowを実現してログイン時のユーザー認証をANDPAD本体のユーザーアカウント情報を用いて行うフローを実装しました。

f:id:teru-yat:20200625211444p:plain
Authorization Code Flow

RPとはRelying PartyのことでOpenID Connectにおける認証クライアントを示します。この場合は今回の新規サービスのAPIサーバーとなります。
IdPとはOpenID Providerのことで認証サーバーを示し、今回のケースにおいてはANDPAD本体です。
Resource Serverは認証によって保護されたリソースを所有するサーバーで、今回はユーザーアカウントを持つANDPAD本体を指します。

  1. まずエンドユーザーは新規サービスの画面上でログインを行います
  2. ログインボタンを押されると、APIサーバーへログインリクエストを行います
  3. APIサーバーは、認証サーバーであるANDPAD本体のAuthorizationエンドポイントへRedirectします
  4. Redirectに従ってANDPAD本体へアクセスします
  5. ブラウザ上でANDPAD本体でログイン処理を行います
  6. ANDPAD本体は設定してある新規サービスのコールバックURLへAuthorization Codeと一緒にRedirectします
  7. Redirectに従ってAPIサーバーのコールバックURLへアクセスします
  8. APIサーバーはAuthorization Codeを使ってANDPAD本体へID Tokenをリクエストします
  9. ANDPAD本体はAuthorization Codeを確認してID TokenをAPIサーバーへ渡します
  10. APIサーバーはそのID Tokenを使ってANDPAD本体にユーザー情報をリクエストします
  11. ANDPAD本体はユーザー情報を返します
  12. 以上をもってログインが完了し、APIサーバーはログイン結果をブラウザへ返します
  13. ユーザーは無事にログインできました

 

途方にくれた話

さて、新規サービスをフルスクラッチで立ち上げようとしていたわけですが、ANDPADのプロダクトの一部であるため、それぞれ別のユーザーアカウントを作らせる仕様は嫌だなと思い、本体ANDPADのユーザーアカウントを利用しようと決めました。

幸い、ANDPAD本体では、OpenID Providerをdoorkeeperを用いて実装されていたので、新規サービスの方ではRelying Partyを実装してAuthorization Code Flowを実現することにしました。
github.com


 
実際にRelying Partyを実装するにあたり、今後社内で同様に新しいサービスにおいてユーザー認証する時に再利用できる形にしたいとは思いつつ、AWS Cognitoを使って認証を行うかDeviseに認証方式を足すか迷いました。
特に、ローカルでの開発時にAWS Cognitoを用いた場合にどうやって処理を行えるのかが不明で調べたりもしました。
いろいろと調べたりしましたがAWS Cognitoはその時点では私時点の知見が少なく、今後のことを考えるとどこまで再利用性が高いのか等が不明で、結局Deviseに組み込む形で実装を考えることにしました。

 
ただ、ここからが問題でした。

いろいろと調べたりもしましたが、実装の参考になるドキュメントがなかなか見つからず、また既存のGemがないかも調べましたが継続的にコミットがあり今後も安心して使っていけそうなものは私が探した限りでは見つかりませんでした。
英語も日本語も探しましたがわりとCognitoを使って実装してしまうケースが多いように見受けられました。


結局、Novさんの作られたopenid_connectというGemと、(Implicit FlowでAuthorization Code Flowとは別ですが)Qiitaのこちらの実装例を参考に試行錯誤しながら実装したので紹介させていただこうと思います。
github.com
qiita.com

ちなみに、ローカルでの開発に関してはdocker-composeにAPI Stubサーバーを追加して認証を擬似的に完了するように実装しました。 

実装詳細

ある程度概略になりますが、実際に先ほど説明したAuthorization Code Flowの流れを追いながら説明していこうと思います。
また、こちらの実装前に認証サーバー側でOpenID Connect 認証クライアントとしてアプリケーションの登録を行っておいてください。
 

(前準備)DeviseにOpenIDConnectの認証の登録

Deviseへopen_id_connect_authenticatableを登録しています。
Deviseでは通常userがscopeになりますが今回はこちらの都合でuser_sessionとしています。

config/initializers/devise.rb

Devise.setup do |config|
  config.warden do |manager|
    manager.default_strategies(scope: :user_session).unshift :open_id_connect_authenticatable
  end
end

Devise.add_module(:open_id_connect_authenticatable, {
  strategy: true,
  controller: :sessions,
  model: 'devise/models/open_id_connect_authenticatable',
  route: :session,
})

認証処理の開始

APIのエンドポイントを叩く際に認証がされているかをまず確認します。

app/controllers/application_controller.rb

before_action :authenticate_user_session!

認証がされていない場合認証を開始しますが、認証自体は認証サーバーであるANDPAD本体で行うのでRedirectします。
この時、stateやnonceをCSRF対策やリプレイ攻撃対策としてセッションに保存します。

app/controllers/web/v1/user_sessions/sessions_controller.rb

  def new 
    redirect_to authorization_uri
  end 

  def authorization_uri
    OpenIdConnect::UserAuthorization.new.authorization_uri(set_state, set_nonce)
  end 

    # CSRF 攻撃対策
  def set_state
    session[:state] = SecureRandom.hex(16)
  end 

    # リプレイ攻撃対策
  def set_nonce
    session[:nonce] = SecureRandom.hex(16)
  end 

この OpenIdConnect::UserAuthorizationはlib配下に作ったmoduleで、環境変数からANDPAD本体のエンドポイント情報等を使ってAuthorizeエンドポイントを返します。
Client ID, Client SecretはこのAPIサーバーをANDPAD本体側で登録した際に生成されます。
callback URLはapp/controllers/web/v1/user_sessions/sessions_controller.rbのcallbackメソッドに返ってくるように登録しておきます。

lib/utils/open_id_connect/user_authorization.rb

module OpenIdConnect
  class UserAuthorization
    def authorization_uri(state, nonce)
      OpenIDConnect::Client.new(
        identifier: <ANDPAD本体で登録時に生成されたclient id>,
        secret: <ANDPAD本体で登録時に生成されたclient secret>,
        host: <ANDPAD本体のhost>,
        port: <ANDPAD本体のport>,
        redirect_uri: <ANDPAD本体で登録時に指定したcallback URL>,
        authorization_endpoint: <ANDPAD本体のAuthorizeエンドポイント>,
      ).authorization_uri(
        response_type: 'code',
        state: state,
        nonce: nonce,
        scope: %w[openid]
      )   
    end 

Authorization Codeを受け取ったら

その後、ANDPAD本体から無事にAuthorization Codeを受け取ったら、ID Tokenを取得してユーザー情報を取得します。
callbackを叩かれたら、Deviseのauthenticate!を呼び出し、無事にユーザー情報を取得できていたら認証を完了します。

app/controllers/web/v1/user_sessions/sessions_controller.rb

  def callback
    resource = request.env['warden'].authenticate!(:open_id_connect_authenticatable, scope: :user_session)

    redirect_to <認証後のトップページURL>
  end

lib/utils/devise/strategies/open_id_connect_authenticatable.rb

      def authenticate!
        oidc_client = OpenIdConnect::UserAuthorization.new
        user_info = oidc_client.verify!(params["code"], session.delete(:nonce))

        # user_infoからuser_sessionを紐づけるもしくは登録する処理

        success!(user_session)
      end 


authenticate!内では、OpenIdConnect::UserAuthorizationのverify!が呼び出されていますがこの中でID Tokenの取得と検証、そしてユーザー情報の取得を行っています。
verify!ではissやnonceなどの検証を行うフィールドを渡してあげてください。何かしらのエラーがあるとここで例外が投げられるので認証が失敗します。

lib/utils/open_id_connect/user_authorization.rb

    def verify!(code, nonce)
      oidc_client = OpenIDConnect::Client.new(
        identifier: <ANDPAD本体で登録時に生成されたclient id>,
        host: <ANDPAD本体のhost>,
        port: <ANDPAD本体のport>,
        redirect_uri: <ANDPAD本体で登録時に指定したcallback URL>,
        authorization_endpoint: <ANDPAD本体のAuthorizeエンドポイント>,
        token_endpoint: <ANDPAD本体のTokenエンドポイント>,
        userinfo_endpoint: <ANDPAD本体のUserinfoエンドポイント>
      )   
      oidc_client.authorization_code = code
      access_token = oidc_client.access_token!

      id_token = decode_jwt_string(access_token.id_token)

      id_token.verify!(
        # 検証を行うフィールドをここで列挙
      )   

      user_info = access_token.userinfo!

      user_info
    end 


また、取得したID Tokenはそのままでは暗号化されているので復号化する必要があります。
今回の実装では、Decodeに必要なKeyをJson Web Key(JWK)で取得しています。
ここらへんは先ほど紹介させていただいたQiitaのこちらの記事でDecodeのサンプルがあったので参考にさせていただきました。
qiita.com

lib/utils/open_id_connect/user_authorization.rb

    def decode_jwt_string(jwt_string)
      OpenIDConnect::ResponseObject::IdToken.decode(jwt_string, jwk_json)
    end 

    def jwk_json
      @jwks ||= JSON.parse(
          OpenIDConnect.http_client.get_content(<ANDPAD本体のJWKS用エンドポイント>)
        ).with_indifferent_access
      JSON::JWK::Set.new @jwks[:keys]
    end

これで、無事に一連の認証のフローを通してユーザー認証を実現することができました。

ローカル環境でのAPI Stubサーバーを使った認証

次に、補足になりますが、ローカル環境でのAPI Stubサーバーの実装を簡単に紹介しようと思います。

前章まででOpenID Connectの認証クライアントはできたので、今回は認証サーバーのStubを作ります。
ローカルでANDPAD本体も起動しても良いのですが、認証のためだけにANDPAD本体も起動するとさすがにローカル環境が重くなるからです。

まず、Ruby on RailsにStub用のパスを切って以下のようにNginxのDockerfileを作ります。

api-stub/Dockerfile

FROM nginx:latest


次に、Nginxの設定ファイルを編集して認証サーバーのAuthorization、Token、UserInfo、JWKのエンドポイントを設定します。

api-stub/conf.d/default.conf

    # OpenID Connect Authorizationエンドポイント
    location /v1/open_id_connect/authorize {
        add_header Access-Control-Allow-Origin *;
        return http://<host>:<port>/v1/callback?code=<dummy code>&state=<dummy state>;
    }   

    # OpenID Connect Tokenエンドポイント
    location /v1/open_id_connect/token/ {
        add_header Access-Control-Allow-Origin *;
        error_page 405 =200 $uri;
        root   /usr/share/nginx/json;
        index  token.json;
    }   

    # OpenID Connect UserInfoエンドポイント
    location /v1/open_id_connect/userinfo/ {
        add_header Access-Control-Allow-Origin *;
        error_page 405 =200 $uri;
        root   /usr/share/nginx/json;
        index  userinfo.json;
    }   

    # OpenID Connect Decode用Keys
    location /v1/open_id_connect/keys/ {
        root   /usr/share/nginx/json;
        index  keys.json;
    }   

そのあとは、api-stub/json配下に実際のOpenID Connectで受け取っていたJSON情報を配置して、それぞれのエンドポイントを叩かれた際にJSON情報が返されるようにします。

以上で、ローカル環境でも認証を擬似的に再現して動作するように実装できました。

最後に

今回のOpenID Connectでの認証に関してはなに分時間のない中で手探りで実装したので大変でしたが、無事に実装ができてよかったです。
しかも、認証の大部分はlib配下に置くことができたので、再利用性も高く作れたのかなと思います。

正直、OAuthであればDeviseが対応してあるので楽に実装できたのかなとは思いますが、OAuthでの認証の脆弱性も認知されておりいつまで利用できたかは分からないかなと思います。
もちろん、プロダクトで必要なセキュリティと開発リソースのバランス次第だとは思いますが、今回は同様新規サービス作成時の再利用性を高めるメリットもあったので良いチャレンジになりました。


まとめになりますが、こちらの自身の実装の共有がこれからOpenID Connectの認証を行う方の参考になればと思います。
もしおかしい点があれば暖かい目で見つつご指摘いただけると幸いです。



ANDPADでは、「幸せを築く人を、幸せに。」をミッションに建築業界のみなさまを支援すべく開発に取り組んでいます。もしよければ以下もご覧ください。

engineer.andpad.co.jp


長文をお読みいただきありがとうございました。 

 

データドリブンな開発をめざして 〜モバイル用分析基盤を整備した話〜

モバイルアプリチームの工藤です。

ANDPADではこれまでユーザの声や思い描くビジョンを元にアプリの開発を進めてきましたが、徐々に機能変更や改善に関する仕事も増えてきています。現在のユーザの状況を意識して議論するシチュエーションも多々あるため、PdMより直々に早急にデータ分析基盤を用意せよとのお達しがありました。

まだまだ粗い部分もありますが、先日一区切りついたのでその対応内容をまとめようと思います

全体の方針を決める

今回の対応で最も意識したのは、複雑なことはしないで専任の担当者以外でも運用・維持ができるようにするという点です。弊社はスタートアップ企業であり、データ分析が根付いている他の企業に比べてまだまだエンジニアは多くありません。明確にアプリの分析担当者が居るわけではなく、職種に関わらずこのデータに触れる可能性があります。

そのため、将来的に突発的な運用作業や謎ルールに苦しめられることを避けられるよう気をつけながら各種設定を行いました。

大まかなデータの流れ

f:id:oct_k_kudo:20200605173304p:plain:w300

  1. アプリからユーザアクションに応じたログを送信
  2. ログデータはBigQueryに格納されるので、ROWデータをRedashで扱いやすいように加工する
  3. 作成した参照用テーブルからデータを取得し、集計してアウトプットする
  4. Slackでメンバーに更新通知する

現状の設計とドキュメント作成

開発対象の案件が確定した段階でPdMと分析要件について相談し、アウトプットのすり合わせをします。テキストで表現しづらいニュアンスなどは、適宜手書きでイメージしているグラフを書いたりしています。

実際に送るログはどの画面のどの操作なのか?を一意に特定できるようにRDBをイメージして探り探り設計していますが、データサイエンティストに相談しながら改善していきたいなと思っているところです。

設計した内容はGoogleスプレットシートにまとめていて、Confluenceにリンクを記載しています。設計書としてバージョン管理する事も考えたのですが、GItHubアカウントを持たない非エンジニアが分析することも踏まえてアクセスのしやすさを優先しました。今はまだ情報量が少ないおかげで成立している部分もあるので、将来的に運用が軌道に乗ってきた段階でVCSを利用することも検討したいです。1

アプリでログを送信する

アプリ用のログ収集ツールいくつか検討しましたが、以下の理由からGoogleAnalyticsを利用することにしました

  • Crashlyticsなどで既にFirebaseを利用している
  • Cloud MessagingやPredictionsなど、連携できる・してみたいサービスが提供されている
  • 検討したどのサービスも導入コストはそれほど変わらなかった

参考として、アプリの案件一覧画面でどの案件がタップされているか?を調査するために実装した内容を一部載せてみます。 少し長いので、興味のある方は展開してみてください。

f:id:oct_k_kudo:20200616155843p:plain:w200
iOSアプリのトップ画面

iOSの実装

FirebaseAnalyticsのラッパークラス

class AndpadAnalytics {
    private init() {}
    static let shared = AndpadAnalytics()
    
    func setUser(id: Int64){
        Analytics.setUserID(String(user.id))
    }

    func sendLogEvent(event: AndpadAnalyticsEvent) {
        let parameters = event.parameters?
            .compactMapValues({$0})
            .mapValues({"\($0)"})
        Analytics.logEvent(event.eventName, parameters: parameters)
    }
}

enum AndpadAnalyticsEvent {
    case selectContent(screenName: String, contentType: String, contentId: Int64? = nil)
    
    var eventName: String {
        switch self {
        case .selectContent: return FirebaseAnalytics.AnalyticsEventSelectContent
        }
    }
        
    var parameters: [String : Any?]? {
        switch self {
        case .selectContent(let screenName, let contentType, let contentId):
            return [
                AndpadAnalyticsConstants.Key.screenName: screenName,
                FirebaseAnalytics.AnalyticsParameterContentType: contentType,
                AndpadAnalyticsConstants.Key.contentId: contentId,
            ]
        }
    }
}

struct AndpadAnalyticsConstants {
    struct Key {
        static let screenName = "screen_name"
        static let viewName = "view_name"
        static let contentId = "content_id"
    }

    struct ScreenName {
        static let 案件一覧画面 = "案件一覧画面"
    }
    
    struct ContentType {
        static let orders = "orders"
    }
}

イベント発火時のコード

button.rx.tap
    .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .background))
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { [weak self] in
        AndpadAnalytics.shared.sendLogEvent(event: AndpadAnalyticsEvent.selectContent(
            screenName: AndpadAnalyticsConstants.ScreenName.案件一覧画面,
            contentType: AndpadAnalyticsConstants.ContentType.orders,
            contentId: order.id
        ))
    })
    .disposed(by: disposeBag)

Androidの実装

FirebaseAnalyticsのラッパークラス

object AndpadAnalytics {
    private lateinit var firebaseAnalytics: FirebaseAnalytics

    // Applicationクラスで初期化しています
    fun initialize(context: Context) {
        firebaseAnalytics = FirebaseAnalytics.getInstance(context)
    }

    fun setProfile(id: Int) {
        firebaseAnalytics.setUserId(id.toString())
    }

    fun sendEvent(event: AndpadAnalyticsEvent) {
        val bundleString = event.data.keySet().joinToString("\n") { "$it: ${event.data.get(it)}" }
        Timber.d("""
event: ${event.eventName}
bundle:
$bundleString
        """)
        firebaseAnalytics.logEvent(event.eventName, event.data)
    }
}

sealed class AndpadAnalyticsEvent {
    abstract val eventName: String
    abstract val data: Bundle

    data class SelectContent(
            val screenName: String,
            val contentType: String,
            val contentId: Long? = null,
    ) : AndpadAnalyticsEvent() {
        override val eventName = FirebaseAnalytics.Event.SELECT_CONTENT
        override val data
            get() = Bundle().also {
                it.putString(AndpadAnalyticsConstants.Key.screenName, screenName)
                it.putString(FirebaseAnalytics.Param.CONTENT_TYPE, contentType)
                it.putString(AndpadAnalyticsConstants.Key.contentId, contentId.toString())
            }
    }
}

object AndpadAnalyticsConstants {
    object Key {
        const val screenName = "screen_name"
        const val viewName = "view_name"
        const val contentId = "content_id"
    }

    object ScreenName {
        const val 案件一覧画面 = "案件一覧画面"
    }

    object ContentType {
        const val orders = "orders"
    }
}

イベント発火時のコード

binding.button.setOnClickListener { 
    AndpadAnalytics.sendEvent(AndpadAnalyticsEvent.SelectContent(
        AndpadAnalyticsConstants.ScreenName.案件一覧画面,
        AndpadAnalyticsConstants.ContentType.orders,
        content.id
    ))
}

ポイントは、

  1. なるべくConstantsを作成し利用する。変数名は送信値をlowerCamelに変換したもの。ただし実装上問題なければ日本語変数も可。
  2. イベントタイプごとに構造体を作っている
  3. データのValueは必ずStringにキャストしている(今はiOSのみ)

あたりでしょうか。3つとも、 実装担当者以外が分析する際にコードを確認するシチュエーションがあるかもしれない という前提で設計されたものです。

1の日本語変数に関しては、歴史的経緯から本来の意味とプログラムの命名がかけ離れているコンポーネントもあり、プログラム名とそこで使用される分析用変数と日本語で表現された役割がバラバラになるよりは、変数と役割だけでも同じものにできれば良いという考えで割り切っています。例えば案件一覧は Ordersと表現したいもののHomeActivityとして作成されており、プログラム内で突然画面名としてOrdersが出てくるよりは日本語で案件一覧と表現したほうが理解しやすい、といった具合です。

f:id:oct_k_kudo:20200616170215p:plain
プルリクのコメント。wが草なのか苦笑いなのかはまだ聞けていません

3はキャストによるオーバーヘッドが気になる人もいるかと思います。Analytics用に作成されるテーブル定義では値の型ごとにデータが格納されるカラムが変わってしまうため2、IDだからNumberだと思っていたらStringになっているのでデータが見つからない!といったトラブルを回避できます(極力一貫したデータを取得したいため、あとから実装ミスに気づいて実装を変更するケースを減らしたいいう意図も含んでいます)。また、String以外で扱いたい場合に必ず集計側で加工するという手間を強制することで、ゴミデータ起因でSQLがある日突然動かなくなるというトラブルを無くせると期待しています。
トレードオフだとは思いますが、今回は少しでも可用性が高くなる方法を優先しました。

データを集計する

BigQuery

Firebaseから連携されたデータはプロジェクト単位かつ日付単位でテーブルが作成されるため、特定アプリに関するn日分の集計ではSQLでwhereやunionが必要です。そこまで複雑なSQLでは無い一方で、テーブルのデータ全てを参照するため3対象テーブルを間違えた際の料金はデータ量を考えると怖いものがあります。

ある程度共通のテーブルを作成すればそこからのデータ取得で実現できるクエリが多そうだったため、共通のビューとそれを使ってOS/アプリ単位のカスタムテーブルを作るクエリを作成し、スケジューリング機能を使って毎朝データの前処理をしています。

必要な日数分だけテーブルを絞り込むビュー用のSQL

SELECT
  -- 他にも必要なカラムは適宜指定する
  CAST(CONCAT(
SUBSTR(event_date, 1, 4), 
'-', 
SUBSTR(event_date, 5, 2), 
'-', 
SUBSTR(event_date, 7, 2) 
) AS date) AS event_date,
  app_info.id AS app_name,
  -- バージョンを数値に変換することで、利用側で判定しやすくしている
  (IFNULL(SAFE_CAST(SPLIT(app_info.version, ".")[
      OFFSET
        (0)] AS INT64),
      0) * 1000000) + (IFNULL(SAFE_CAST(SPLIT(app_info.version, ".")[
      OFFSET
        (1)] AS INT64),
      -- デバッグビルドではX.Y.Z-debugのようなsuffixが付与されるので、それを除外するコード
      0) * 1000) + IFNULL(SAFE_CAST( SPLIT(SPLIT(app_info.version, ".")[
      OFFSET
        (2)], "-")[
    OFFSET
      (0)] AS INT64 ),
    0) AS app_version,
  event_params AS event_params,
  user_properties AS user_properties
FROM
  `********.analytics_**********.events_*` t  -- 一部伏せ字にしています
WHERE
  -- ここで必要な日数分のテーブルを指定する。一旦は7日分
  _TABLE_SUFFIX BETWEEN FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY))
  AND FORMAT_DATE('%Y%m%d', DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY));

実際のところ、上記クエリの作成中はテスト実行だけで毎回料金が発生しているという事実がそれなりにストレスだったので、何より自分のためにやってよかったと実感しています

f:id:oct_k_kudo:20200616165631p:plain:w500
iOSアプリ用のクエリの実行履歴

Redash

Redashはもともと社内で一部利用実績があったため採用しました。 特に複雑なことはしておらず、クエリをスケジュール実行してダッシュボードで一覧できるようにしています。

先のサンプルコードからどの案件がタップされているか?を集計した場合のSQLを紹介します。BigQueryは配列型がいまだに慣れません

WITH dataSet AS
  (SELECT event_date,

     (SELECT unnest_event_params.value.string_value
      FROM unnest(event_params) unnest_event_params
      WHERE unnest_event_params.key = "screen_name") AS screen_name,

     (SELECT unnest_event_params.value.string_value
      FROM unnest(event_params) unnest_event_params
      WHERE unnest_event_params.key = "content_type") AS content_type,

     (SELECT unnest_event_params.value.string_value
      FROM unnest(event_params) unnest_event_params
      WHERE unnest_event_params.key = "content_id") AS content_id,

   FROM `**********.analytics_**********.custom_table_firebase_analytics_ios_app`
   WHERE event_name = "select_content"
     AND app_version >= 1002003) -- この例だとバージョン1.2.3
SELECT event_date,
       content_id,
       count(*) AS COUNT
FROM dataSet
WHERE screen_name = "案件一覧画面"
GROUP BY event_date,
         content_id
ORDER BY event_date,
         content_id;

Slack通知

RedashのSlack APPの活用も考えましたが、ダッシュボードは利用できないそうなので一旦はリマインダーをセットしてリンクを通知するようにしています。まだ具体的なアクション起こせていませんが、毎朝ダッシュボードを確認する習慣をつけ、皆がデータを意識して案件に取り組めるようになれば良いなと考えています

f:id:oct_k_kudo:20200616172234p:plain

まとめ

エンジニアが主体となって集計できるようになったことで、最近はチーム内でもデータを元に既存機能に関する議論をすることが増えてきました。今後は今のダッシュボードを充実させて総合的に良し悪しの判断できる軸を作っていきたいです。

f:id:oct_k_kudo:20200616175839p:plain:w300

とはいえ、取得したデータはまだ完全に活かしきれていないのが現状です。これからもデータドリブンな開発をしユーザビリティの高いサービスを提供するためには、今まで以上にたくさんの仲間が必要だと考えています。

もし少しでも興味がありましたら弊社採用サイトもご覧になってください。

engineer.andpad.co.jp

最後までお読みいただきありがとうございました


  1. 他の会社さんはこの辺どうしているんですかね?

  2. keyに対して、int_valueやstring_valueとしてカラム定義されています

  3. BigQueryでは最初に参照したデータ量で金額計算が行われるため、whereやlimitは金額面で考慮されません

社名変更して co.jp ドメインを複数保有する技術

SREチーム 鈴木心之介 です。 職歴の空白 を経て参画しました。

先日、株式会社オクト (88oct.co.jp) だった弊社は、社名変更し、株式会社アンドパッド (andpad.co.jp) になりました。社名変更といえば、めちゃくちゃ、それはもう大変な量の仕事がありますが、ここでは社名変更して co.jp ドメインを複数保有する話を書きます。

短い結論

いま保有している co.jp ドメインを、JPDirerct に移管しましょう。JPDirect は、 jp ドメインレジストリである株式会社日本レジストリサービス(JPRS) が運営するレジストラで、1組織1ドメインの緩和申請に対応していることを表明しています。

JPDirectの管理画面で新ドメインを仮登録すると、直ちにDNS設定を組めるようになります。仮登録から6ヶ月以内に社名変更の法人登記を完了し、1組織1ドメイン制限緩和の申請を完了する必要もあります。このため仮登録を何月何日に実施するかは、意外に重要な要素なので、スケジュールを慎重に検討してください。

長い話

弊社は旧社名のドメインを、お名前.comで購入、更新していました。

社名変更の話が本格始動したときに、ドメイン変更についての私を含めて弊社メンバーの認識は、

  • ドメイン取得時は、法人登記やハンコやら書面提出が必要であること
  • 1組織1ドメインの制限があること
  • net や xyz みたくシュッと買えるでしょ。即日で

ヤバいです。誰もわかってないです。炎上プロジェクトです。幸いにして日程はフィックスしていなかったので調べまくりました。

お名前.comのサービスの制限

お名前.comでは、「ドメイン名変更」と「登録者情報の変更(記載事項変更)」によって、新旧ドメインを一定期間、持っておくことができるとわかります。

一定期間については以下に記載があり、特に旧ドメインは6ヶ月で利用できなくなります。また「旧ドメイン名の文字列は廃止後5ヶ月間は登録不可となる」のため、その後は別の法人が取得可能となります。

help.onamae.com

まあそんなもんかと思いつつ、いや待て。旧ドメインは、会社のメンバーのメールアドレスにも使っていて、名刺にも印刷しており、お客様にも広く配っています。仮に、旧ドメイン強制廃止の6ヶ月を過ぎて、登録不可の5ヶ月も過ぎて、ぜんぜん弊社と関わりのない企業が、旧ドメインを取得されたとします。そのとき古い名刺のメアドに、お客様からメールが送られたら、たいへんややこしい事態になります。それは避けたい。

新旧の両ドメインを恒久的に保持したい

お名前.comの制約と向き合うことで、我々の本当の欲求が理解できました。新旧ドメインを、恒久的に保持したいのです。

ところで1組織1ドメインってどこから湧いてきたのですか。所与の制約みたく言ってますけど。その謎を探るべくググってみるものの、数年前の古い情報だったり、真偽の定かでない情報だったり、素性の怪しい業者の怪しいページばかりが引っかかりました。つらい。社名変更ほんとにやるんすか。そんな声が主に私から上がります。

co.jp は 1組織1ドメインしか保有できません、が、、、

ほうぼう調べて、JPRSの属性型JPドメインの規則が出所とわかり、以下の理解に至りました。勘所がなかったので遠回りになりました。

トップレベルドメインのひとつである .jpレジストリは、株式会社日本レジストリサービス(JPRS)です。お名前.comなどはレジストラです。我々平民がレジストラからドメインを購入しますと、レジストラレジストリに、「お名前.comで、株式会社オクトが、 88oct.co.jp を1年購入した。法人登記はコレです」のように情報を上げます。レジストリは、ドメインの利用状況であったり、法人登記の確認を行なったりします。

.jp ドメインは「汎用JPドメイン」。対して co.jp, go.jp, ac.jp などは「属性側JPドメイン」と呼びます。とJPRSが定義しています。

jprs.jp

ページの下のほうに「属性型(組織種別型)・地域型JPドメイン名登録等に関する規則」というリンクがあります。

jprs.jp

「第9条(登録できる属性型地域型JPドメイン名の数)」として、「登録できる属性型地域型JPドメイン名の数は、1組織について1とする」が謳われています。しかし、続く項として、合併、組織名変更、事業譲渡などの場合は、「1組織について2以上の属性型地域型JPドメイン名の登録をすることができる」とも定められています。

JPRSが、属性型jpドメインに関する規則として、そのように定めて運用しているのですね。

ところで、その規則に則って対応してくれるレジストラは?

JPRS直営のレジストラ「JPDirect」

そんな都合のいい組織があるわけが、あったわ。やたら情報の解像度の高いページがあり、問い合わせ窓口もあり、藁にすがる思いで泣きつきました。

jpdirect.jp

料金はこのようで。お名前.comに慣れた金銭感覚からすると、なかなか強気の料金です。

jpdirect.jp

ここで損得勘定です。

実のところ別法人を立てることで、お名前.comに置いたまま新旧ドメインを恒久的に保持することも、論理的に可能です。別法人なら法人登記が異なりますので。

www.onamae.com

決定打を欠き、弊社コーポレート部門の長を巻き込んでみたところ、社名変更で新旧ドメイン保持のためだけに別法人を立てるのと比べたらJPDirect安い!!との声を頂戴し、JPDirectで進めることとしました。

JPDirect にアカウント開設

https://jpdirect.jp の右上「ドメイン管理ページ」から「お客様IDの新規取得はこちら」のリンク先で、アカウント開設ができます。

f:id:sasasin_net:20200608233213p:plain

JPDirect に旧ドメイン移管

いやこれ必要か?と思ったでしょうが、JPDirectが、社名変更にかかる1組織1ドメイン制限緩和の申請の条件として、新旧ドメインをJPDirectに置いていることとあるため、移管してきます。

お名前.comとJPDirectの両方で、所定の手順に則って進めます。だいたい2時間くらいで終わります。お名前.comでのDNS設定を引き継いで、JPDirectに移管されるので、寸断もありません。

help.onamae.com jpdirect.jp

JPDirect で新ドメイン取得

ドメインは、いつでも仮登録で取得でき、DNS設定を組めるようになります。

弊社ではAWS Route 53を使っていますので、新ドメインのHosted Zoneを作り、NSレコードをJPDirect側の新ドメインに入れたら、nslookupなどで開通確認できるようになります。

なお仮登録から6ヶ月以内に「1組織1ドメイン制限緩和の申請」を完了し、本登録も完了する必要があります。

1組織1ドメイン制限緩和の申請

JPDirectが仲立ちして手続きが進みます。社名変更の法人登記手続を終えて、社名変更の事実を証明する書面等を用意して、一部はFAXで送るなど慣れがないと難しいワークフローです。

「法人登記の手続を終え」があるため、あまり早い日に新ドメインを仮登録すると、たいへん難しい状況になる可能性があります。余裕を取りすぎると、新ドメインでのメアドや名刺、コーポレートサイトの制作の日程も組みづらくなるため、仮登録をいつやるか難しさがあります。

ドメインの本登録

ドメインを仮登録した場合は、本登録する必要もあります。こちらは旧ドメインを取得し本登録された時と同様ですが、どのような書類が必要かは、JPDirectにご確認ください。

むすび

社名変更に立ち会う機会は初めてで貴重な経験でした。皆様が円滑に社名変更と co.jp を複数保有されることを祈ります。長文お読みいただきありがとうございました。

WordPressをShifterへ移行した話

はじめに

はじめまして、今年1月からSREチームに加入した千明です。

SREと名乗っているものの、前職ではバックエンドエンジニアが本職だったこともあり、インフラ構築以外にもアプリケーションの改修もしたり、幅広く業務にあたっております。

まだまだSREとしてはひよっこですが、会社に貢献できるように日々努力しているところです。

今回は直近でコーポレートサイトをリニューアルしたこともあり、AWS上で管理していたWordPressをShifterへ移行をした話をしたいと思います。

andpad.co.jp

なぜ移行したのか

主な理由は、WordPressを面倒見るSREの人的コストを削減するためです。

WordPressは本体やプラグイン脆弱性が見つかることがあるため、最新の状態であることが推奨されます。

ですが、WordPressは更新頻度が高く、更新するにも事前検証が欠かせないため、自らが全ての面倒をみると多くの人的コストがかかります。

そうなると、ANDPADの成長にコミットする時間が減ってしまうため、外部サービスへ移行することとしました。

Shifterとは

ShifterはWordPressホスティングサイトの1つで、以下のような特徴があります。

  • WordPressのサイトを静的化したもの(Artifact)をCloudFrontで公開する。
  • Artifactは世代管理されていて、以前のバージョンにロールバックできる。
  • Artifactは公開前にプレビュー環境にて動作確認できる。

www.getshifter.io

ShifterはWordPressを直接公開しないので、WordPress本体やプラグイン脆弱性を心配する必要もないですし、投稿者が更新ミスをしてもプレビュー環境で気づくことができ、間違って公開してもロールバックできるので、SREが面倒を見なくても運用ができる仕組みが揃っています。

どうやって移行したのか

基本的にはAll-in-One WP Migrationプラグインを利用してサイトのバックアップを取って、Shifterへインポートするだけでした。

ja.wordpress.org

ただし、既存サイト側の問題でいくつか改修が必要なケースがありました。

  • 固定ページで動的にページ送りをしている部分が静的化の対象にならず、アーカイブページで作り直した。
  • 投稿アーカイブページを利用するために、functions.phpの中で明示的にページを追加する必要があった。
  • 動的フォームを利用するため、別の外部サービスが必要だった。(弊社ではformrunというサービスを利用しています。)

form.run

以上、移行する際に様々な問題がありましたが、ShifterではShifter-LocalというDockerで動くLocal環境が提供されているので、手軽に事前検証や改修を行うことができました。

github.com

また、どうしても困った時でも問い合わせすると1営業日以内に返信していただけるので、とても助かりました。

移行してどうだったか

まだ移行したばかりでまだ完全に手離れできていませんが、テーマやプラグインの更新作業も投稿者側でできるようになったので、運用が定着すれば、面倒を見ることはなくなりそうです。

更新内容は事前にプレビュー環境で確認できるので、万が一プラグイン側に問題があってもロールバックできるので便利だと思います。

現在取り組んでいること

現在はサイトの品質向上や冗長性を考慮して以下の2点に取り組んでいます。

アクセスログをDatadog Logsで可視化

最近ShifterのアクセスログをS3バケットへ出力する機能が追加されました。

アクセスログをDatadog Logsに投入して可視化することで、サイトの利用状況を分析したいと考えています。

www.getshifter.io

障害時の体制整備

Shifterが何らかの原因でサービス障害が起きてしまっても、ドメインを切り替えてサイトを公開できる体制を整備しています。

ShifterのWebhookを利用して、Artifactを自動で別のホスティングサービスへデプロイして公開する仕組みを検討中です。

おわりに

今回はWordPressの話でしたが、便利な外部サービスを利用することで人的コストを減らせることができました。

ANDPADもまだまだ成長段階で、サービス開発していくために多くの人手が必要なので、SREとして管理の手間が省けてとても助かっています。

ANDPADも建設業界の方々にとって、より便利なサービスにしていきたいですね!

f:id:yuichi-chiaki:20200603132532p:plain