オウチーノ開発者ブログ

「株式会社オウチーノ」の社員によるブログです。

Web画面を印刷するための基本知識とハック

どうも。オウチーノの田淵です。この記事はくふうカンパニーアドベントカレンダーの8日目として書いています。

2018年10月に、住宅・不動産専門サイトを運営する弊社と、結婚式場紹介口コミサイトを運営するみんなのウェディングが経営統合し、共同持株会社「くふうカンパニー」が設立しました。くふうカンパニーは現在、結婚関連事業、不動産関連事業の他、金融関連事業、デザイン・テクノロジー事業、投資・起業家支援事業を行っています。

はじめに

私は現在、不動産営業支援ツール「くらすマッチ」を開発しています。
くらすマッチは不動産会社・不動産店舗が簡単に物件周辺の暮らしに関する情報を調べることができ、住宅を探しているユーザーニーズに合わせて提案できるツールです。

Webだけでも完結するサービスなのですが、物件情報と合わせて周辺情報の資料をお客様にお渡ししたいというユーザー(不動産営業)ニーズが高いため以下のような印刷機能を開発しました。

  • ブラウザでプレビューできる
  • ブラウザの印刷機能を使って資料を作成できる

開発時にいざ印刷してみると各要素のスタイルが満足に反映されませんでした😓
印刷のスタイルにはブラウザとは一味違う指定が必要だったので、ここで少しまとめてみます。

前提

ターゲットにしたブラウザ

Chrome、IE11、Edge、Firefox
(ツールの特性上、ユーザー数が多いブラウザをターゲットに開発しています)

用紙サイズ

A4 縦

まずは印刷のための基本

  • メディアタイプを知る
    all すべての機器(ブラウザ、印刷 etc...)
    print 印刷プレビュー
    screen ブラウザ
    他にもありますが割愛。
    メディアタイプ(MDN)

  • CSSファイル内では@mediaを使いブラウザと印刷で適用されるスタイルの条件を振り分ける

@media screen { ブラウザだけに適用する }  
@media print { 印刷プレビューだけに適用する }  
@media print, screen { ブラウザ・印刷プレビューどっちにも適用する }  

@media 規則の例(MDN)

  • CSSの読み込み
<link rel="stylesheet" media="all" href="hoge.css" />

メディアクエリにmedia="all" が指定されるようにします。
(HTMLで省略した場合の既定値はallですがHamlなどを使っている場合はデフォがscreenだったりするので要注意)
メディアクエリのついた条件付きのリソース読み込み(MDN)

A4サイズのページを作る

  • ページサイズの指定
    印刷するページのスタイルには@pageで指定します。
    今回はA4の縦ページにしました。
@page {  
  size: A4 portrait;  
  margin: [ページの余白を指定];  
}

@page(MDN)

  • 改ページの指定
    レイアウトされたページごとに印刷したいので指定の位置で改ページするようにします。ページブレイクのCSSプロパティは3種類あるので駆使して理想の位置で改ページさせます。
@media print {
  .pageBreaker {
    page-break-before: always;
  }
}

page-break-before(MDN)
page-break-after(MDN)
page-break-inside(MDN)

ブラウザ仕様が異なる

上述の印刷用のメディアクエリやプロパティを使うことで印刷時にもブラウザと遜色なくレイアウトでき、スタイルも適用することができます。
ですがご想像の通り、ブラウザによって印刷時のスタイルの仕様は異なります。
Web画面として表示するときの仕様ともまた異なります。

一番困るのが、印刷時に背景色が表現されないこと。
ブラウザによって対応方法が違います。

Chrome

背景色を表示するには-webkit-print-color-adjust: exact;を指定する必要があります。
また表示したい背景色には!importantも必要です。

body {
  -webkit-print-color-adjust: exact;
  background: #F4F4F4 !important;
}

IE11

背景色を表示するにはユーザーがブラウザの印刷プレビュー上のページ設定で背景の色とイメージを印刷するのチェックボックスをONにしないと表示されません😓

Edge

Edgeは背景色が表示されません。
(」゚O゚)」< Edgeは背景色が表示されません。
方法を探し回りましたが、↓↓の回答に行き当たりました。マイクロソフトサポートの公式回答のようです。
Windows10 EdgeでWeb上の画面を印刷すると背景の色や罫線が印刷できません。

まことに恐れ入りますが、Microsoft Edge には Internet Explorer のように背景を印刷する設定箇所がありません。 そのため、背景を印刷される場合は IE をご利用頂くようお願いいたします。

現在でも設定できそうな箇所は見当たりませんでした😭

Firefox

背景色を表示するには表示したい背景色に!importantが必要です。

background: #F4F4F4 !important;

また、ユーザーがブラウザの印刷設定でアピアランス > 背景色をプリントのチェックボックスをONにしないと背景色は表示されません😓

Edgeの背景色問題をどうしたか

背景色background-colorが表示されないEdgeでもborderや画像は表示されるようです。
background-imageを指定してbackground-colorの代替とする手法がWeb検索するとヒットしましたが、カラーごとの画像を用意するのは変更に弱い印象を受けました。

当時は開発中のためデザイン的にも容易に変更できるほうが望ましかったのです。
幸いレイアウトも単純だったためborder-colorを利用して背景色を表現することにしました。

一例としてタイトル行に背景色をつける例。

HTMLとCSSはこんな感じ。

<div class="titleBackground"></div>
<h3>
    <span>No</span>
    <span>コンビニ</span>
    <span>距離</span>
</h3>
@media print {
  .titleBackground {
    border-bottom: 20px solid #272736 !important;
    margin-bottom: 10px;
  }

  h3 {
    height: 20px;
    font-size: 8px;
    margin-top: -30px;
  }
}

背景色の代わりにborder-bottomの線幅を欲しいサイズに指定しています。
その下に用意したタイトルになるテキストをネガティブマージンでborderに重ねてタイトル行の背景色を表現しました。

Googleマップからマーカーがはみ出る問題

サービスの特性上、周辺情報を表示するためにマップは不可欠です。当然、紙面にもマップが印刷されます。
今はGoogle mapを利用しています。
スーパーやコンビニ、公園、保育園などの周辺施設の位置をカスタムマーカーで表現しています。

地図の縮尺によってはレイアウト内に収まらないマーカーが出てきます。当然、Googleマップではレイアウト外になったマーカーは表示されない動作になります。

これがIE11では動きが異なります。
レイアウト外になったマーカーがマップ外に表示されてしまいます。
印刷時にもマップ外の領域に突然マーカーが表示されている状態になるので困ります。

Google mapはデフォルトで下記のようなマークアップがされています。
いろいろ試したところ、class="gm-style"のdiv要素にoverflow: hiddenを指定してあげることでこの問題は解消できます。(しかも他のブラウザでの悪影響は発生しません)

<div class="map" style="position: relative; overflow: hidden;">
  <div style="height: 100%; width: 100%; position: absolute; top: 0px; left: 0px; background-color: rgb(229, 227, 223);">
    <div class="gm-style" style="position: absolute; z-index: 0; left: 0px; top: 0px; height: 100%; width: 100%; padding: 0px; border-width: 0px; margin: 0px;">
      <div tabindex="0" style="position: absolute; z-index: 0; left: 0px; top: 0px; height: 100%; width: 100%; padding: 0px; border-width: 0px; margin: 0px; cursor: url(&quot;https://maps.gstatic.com/mapfiles/openhand_8_8.cur&quot;), default; touch-action: pan-x pan-y;">
      <div style="z-index: 1; position: absolute; left: 50%; top: 50%; width: 100%; transform: translate(0px, 0px);">
      <div style="position: absolute; left: 0px; top: 0px; z-index: 100; width: 100%;">
      <div style="position: absolute; left: 0px; top: 0px; z-index: 0;">
<以下割愛>
.gm-style {
  overflow: hidden;
}

まとめ

今後も細かい調整や各OS、各ブラウザへの対応をブラッシュアップしていきたいと思います。

Web画面をPDF化せずに印刷するにはブラウザの印刷機能を使うしかなく、JavaScriptからも制御できない部分が非常に多いです。
ページごとのヘッダー・フッター表示などユーザーの印刷設定による部分が大きいので、技術的にできることは少ないがユーザーがストレスなく印刷できるための操作説明などをうまく見せることも必要かと感じました。

普段、Web画面と戦っているエンジニア・デザイナーが画面を印刷したいと言われたときに困らないよう、基本的なところをまとめてみました。

明日(9日)は@ken1flanさんの「ニャーQL勉強会(エンジニアじゃないひとたちとやったSQL勉強会)」です。

Rails+ReactなSPAサイトでSEOをしようとしてぶつかった壁

Rejectcon 2018 レポート記事

こんにちは。オウチーノ吉川です。

去る9/29にRejectconで「Rails+ReactなSPAサイトでのSEO」について登壇してきました。 SPAサイトでSEOを行う際にぶつかった壁と、それをどのように乗り越えていったかの経緯をご紹介しました。

ちょっと内容詰め込みすぎて途中途中端折ってしまったのと、発表後にもいろいろ質問などをいただいていたのであわせて補足しながらご紹介します。

発表資料はこちらです。

なぜ経緯を紹介したのか?

昨今のフロントエンド技術の進化速度は非常に速く、どんどん新しい技術が登場しています。 記事などでどういった技術が登場しているのかは抑えているが、実際に全てちゃんと触ってはいないという方も多いのではないでしょうか。

SSRをするためにはreact-railsやhypernovaがある、ということを知っているだけでは、いざ使うとなったときにどちらを導入すべきか判断できません。 Dynamic Importsについても、利用ケースを知らなければなぜこんな面倒なことをするのか、という印象を持つかもしれません。

今回の対応を進めるにあたって、対応を進めるに連れて昔読んだ記事にたどり着いて、最初からヒント読んでたのに!となったことが何度もありました。 そのため背景をお伝えすることが重要だと考えました。

React-HeadによるSSR

時間がなくて端折ったところです。

React-HeadはSSR対応head管理ツールです。コンポーネントとしてmetaタグなどを設定しておけば、head部分に適切にタグを挿入してくれます。 と、CSRの場合はそれで良いのですが、SSRの場合は全体のレイアウトを別途持っているはずで、その適切な場所に入れ込む必要があります。

React-Headのアプローチは、HeadProvider にArrayを渡しておけば、コンポーネントとしてセットされたmetaタグなどを集めてArrayに入れておいてくれます。そのArrayのみを別途renderToString して差し込んでやるというアプローチです。 これ自体もなかなか複雑なアプローチですが、あくまでNode.jsを想定したアプローチで、hypernovaの場合はそのmetaタグコンポーネントを保持しているのがhypernova、テンプレートはRails側にあるためプロセスをまたいで渡す必要が出てきます。 そこでhypernovaがレンダリングしたルートコンポーネント部分のhtmlにカスタムタグとしてくっつけてRailsに返し、Rails側で文字列操作で適切な場所に入れ替えるという力技で対応しています。

結局SSRはSEOのために必要だったの?

比較検証ができていないので断言はできませんが、SEO上は必須ではないと考えています。 オウチーノの事例ではSSRを導入してもインデックス状況は改善せず、パフォーマンス改善によってインデックス状況が改善しました。 またSSRしないと駄目だというならそもそも全くインデックスされないはずです。 現状もパフォーマンス改善のためにSSRを活用していますが、これはSSR導入済みだったためそれを活用しただけで、必須だったわけではありません。

SSRにすることでのメリットもありますが、メンテナンスコストは高めです。 一番のコストはSSRが壊れないようにすることです。Universal JSでない実装をしてしまって壊すことは日常的にありえますが、 SSRに失敗していてもCSRになるだけなので手元で開発していても気づかない場合があります。 現状はE2Eテストを使ってリリース前になるべく気づけるようにしてはいますが完璧というわけではありません。 またオウチーノの場合はhypernovaを利用しているため、hypernovaサーバーを別途運用する必要があります。 Docker化していてECSで運用しているためすごく手間というわけではありませんが・・・

RailsでSSRするのに疲弊しているように見えるけれど、Railsにのせる必要はあるの?

技術的な側面から言えば必要ではありません。エコシステムとの親和性を考えると、サーバー側はRailsではなくNode.jsがやりやすそうではあります。

一方でオウチーノは現在「全体をモノリスにする」という方針で進めています。 このあたりの背景については以前の記事をご参照ください。 https://developers.o-uccino.com/entry/2018/08/30/152838

今後チームが成熟するにつれてSPAは独立させることも検討しています。

sagaやReact-Headの部分は、あらかじめRails側でpropsを作って渡す形にすればシンプルになるのでは?

最初からSSRを想定した設計にするならそういったアプローチも可能だと思います。 SSRを想定しておらず、routingもすべてReact側にやらせている設計だったのと、SSRにすることで問題が解決するかどうかわからない状況下で 大きく設計を変更すべきでないと判断しこういった対応になりました。

これからそういった設計に変えることもできるのですが、どちらかというとSSRしなくて良い状態にできればベターと考えています。

E2Eテストはどうやってやっている?

変更がマージされた後まずRSpecやJestなどによる各種テストを行っています。 RSpecのfeature specとしてCapybara + Headless Chrome によるテストを行っています。 ただしこの段階ではhypernovaサーバーは使っておらずCSRによるテストです。

テストが通った後asset compileやdocker buildを経てstagingに自動デプロイされます。 stagingへのデプロイ後にE2Eテストをstagingに対して実行しています。 こちらはCapybara + PhantomJSを利用しています。

メジャーシナリオに対して期待した操作ができることに加え、SSRが実行された上でのhtmlが返却されていることや、意図通りのメタタグが出力されていることなどをテストしています。 このE2Eテストが通ったdocker imageに対してタグを付与しており、productionへはこのタグが付与されているものしかデプロイできないようになっています。

なおここでPhantomJSを利用しているのは苦肉の策です・・・ 以前Chromeでは問題ないのにSearch Console上ページがレンダリングされないというエラーが発生したことがあり、 PhantomJSだけがそのエラーを再現できたためこういった構成になっています。 どうやらGoogleのクローラーが利用しているChromeは最新というわけではないらしく、最新のHeadless Chromeを利用していると気づけないケースでした。 ただし現在PhantomJSの開発は停止しているため、Headless Chromeに戻すべきかどうかは検討中です。

このあたりのビルドパイプラインまわりはまた改めて記事にできればと思います。

最後に

いかがだったでしょうか。今後もオウチーノではユーザーにより使いやすいサービスを提供するために技術的なチャレンジを進めていきたいと考えています。 オウチーノでは一緒に働くメンバーを募集中です

GrafanaとPrometheusとTwilioを使ってサーバー監視システムを構築する

オウチーノのSREチームの尾形です。

今回はオウチーノのサーバー監視の仕組みについてご紹介したいと思います。

経営方針が変わる前のオウチーノのサーバー監視は全て外部会社にお願いしていまして、その時は個別の依頼ベースで監視対象を追加・削除してもらっていました。自分達で管理していない状態なので、監視項目に漏れがあったり柔軟な設定が出来ない、Opsへの認識が薄くなるなどの問題がありました。そこで自分達のシステムは自分達で面倒を見ようということで去年から監視をする仕組みを構築し始めました。外部会社ではZabbixを利用していたので、その設定をそのまま頂くことも出来たのですが秘伝のタレを頂いても管理が出来ないということでゼロから構築することを決めました。
SREチーム以外にも見てもらえるグラフを表示出来るツールということでGrafana+Prometheusで構築しました。オウチーノではAWSを利用しているため、AWS CloudWatchで取得できる情報はAWS CloudWatchに任せたりしています。

Grafana + Prometheusの構築方法については今回割愛します。

監視の仕組み

GrafanaにDashboardを表示し、オウチーノドメインのGoogleアカウントを持っている人なら誰でも見ることの出来る状態にしています

CloudWatchMetrics

こんな感じにDashboardを用意し、開発者に気軽に見てもらえるようにしています。もちろん開発者がDashboardやグラフを作ることも可能です。
各グラフにアラートの閾値を設定し、閾値を超えたものに関してグラフ付きでSlackに通知します。

SlackNotification

PrometheusではEC2上に乗っているMySQLのメトリクスやヘルスチェック、EC2のCPUやメモリなど様々な値を収集しています。
現在はまだ出来ていませんが、コンテナ単位のメトリクス取得もする予定です。

Prometheusのデータ収集は各種Exporterを利用しています。監視をOFFにしたい時もあるので、その場合はEC2のTagで制御しています。
prometheus.ymlに以下のような設定値を追加し、PrometheusNodeExporterタグがenabledのものだけを収集するようにしています。

  - job_name: ec2-node-exporter
    ec2_sd_configs:
      - region: ap-northeast-1
        refresh_interval: 60s
        port: 9100
    relabel_configs:
      - source_labels: [__meta_ec2_tag_PrometheusNodeExporter]
        regex: (enabled)
        action: keep
      - source_labels: [__meta_ec2_tag_Name]
        target_label: name

サーバーの追加

Itamaeを利用してサーバーを構築しています。
(ItamaeはChefを簡潔に記載出来るように作られたOSSです。)
共通化したレシピを用意してあるので、各サーバーのレシピを書く際に単純にincludeすればexporterが入ります。

e.g.

include_recipe "../../cookbooks/prometheus-ec2-exporter/default.rb" # この1行を追加するだけ
include_recipe "../../cookbooks/timezone/default.rb"
include_recipe "../../cookbooks/hostname/default.rb"

execute "apt-get update"

...

構築後expoterが起動すると設定に則りメトリクスが収集されます。共通化されたレシピを利用している場合は既にGrafana上にグラフがあるので、特別な設定をしなくてもグラフ上に構築したサーバーが表示され、アラート監視の管理下に入ります。
ItamaeのレシピはGit管理下にあるため、GitHubのPR時のレビューで監視の漏れがないかをチェック出来、1度レシピを作ればSREチームでなくとも簡単にサーバーを構築・監視下に置くことが可能です。

オンコールの仕組み

Slack通知だけでは夜間・休日に気づくことが難しくなります。なのでSlack通知の他に電話通知もしています。
電話発信にはTwilioという音声通話をAPIでコントロールできるサービスを利用し実現しています。
大まかな流れは以下のようになります。

  • Googleカレンダーに予め第1窓口、第2窓口の人を週替わりで登録しておく
  • Prometheusがアラートを検知
  • Googleカレンダーに登録されている第1窓口の人に電話
    • 電話を受けた第1窓口の人はチャットボットに対応しますコマンドを投げる
  • 約10分第1窓口の人から応答がなければ第2窓口の人に電話

ここからは実現方法を簡単に説明していきたいと思います。

Googleカレンダーに予め第1窓口、第2窓口の人を週替わりで登録しておく

Google Spread Sheetsにマスターを登録して、日々スクリプトでカレンダーを更新しています。
ここではオンコールだけでなく、トイラーも登録しています。トイラーについては前回のブログのCTOのスライドをご参照ください。

OncallMasterSheets OncallCalendar

スケジュールを更新するスクリプトはGoogle Apps Scriptを利用していて、Google Apps Scriptのスケジューラ機能で日々Google Spread Sheetsのマスターベースに最新のカレンダーに更新します。順番を変更したい場合はGoogle Spread Sheetsをいじってもらえれば次の日には次週以降の情報が更新されます。
(スクリプトの内容については目を瞑って頂けると٩( ᐛ )و

var spreadSheetUrl = 'https://docs.google.com/spreadsheets/xxxx';
var calendarId = 'XXXX'

var columnDefinitions = { 'オンコール': 1, 'トイラー': 2 }

function users(target) {
  var column = columnDefinitions[target];
  var spreadSheet = SpreadsheetApp.openByUrl(this.spreadSheetUrl);
  var sheets = spreadSheet.getSheets();
  var sheet = spreadSheet.getSheetByName('シート1')
  
  var row = 2; // 1行目は列タイトルなので2行目から
  
  var targetUsers = [];
  while(true) {
    var user = sheet.getRange(row, column).getValue();
    if(user == "") {
      break
    }
    targetUsers.push(user);
    row++;
  }
  return targetUsers;
}

function previousMonday() {
  var today = new Date();
  return new Date(today.getFullYear(), today.getMonth(), today.getDate() - today.getDay() + 1);
}

function calenderEvents(seachQuery) {
  var startTime = previousMonday(); // 月曜始まり
  var endTime = new Date(startTime.getFullYear(), startTime.getMonth() + 4, startTime.getDate());
  return CalendarApp.getCalendarById(calendarId).getEvents(startTime, endTime, {search: seachQuery});
}

function removeAfterNextWeekEvents(target) {
  var events = calenderEvents(target);
  if(events.lentgh == 0) {
    return;
  }
  events.slice(1, events.length).forEach(function(element) {
    element.deleteEvent();
  });
}

function addCalender(target) {
  var oncaller = users(target);
  var primaries = oncaller.slice();
  var secondaries = oncaller.slice();
  var findIndex = 0;
  var endTime = previousMonday(); // 月曜始まり
  
  var events = calenderEvents(target);
  if(events.length != 0) {
    var lastEvent = events.pop();
    findIndex = oncaller.indexOf(lastEvent.getTitle().match(/Primary: (.*), Secondary: (.*)/)[2]);
    endTime = lastEvent.getEndTime();
  }
  
  // rotate
  for(var i = 0; i < oncaller.length; i++) {
    if(i < findIndex) {
      primaries.push(primaries.shift());
    }
    if(i < findIndex + 1) {
      secondaries.push(secondaries.shift());
    }
  }
  var calendar = CalendarApp.getCalendarById(calendarId);
  
  for(var i = 0; i < primaries.length; i++) {
    calendar.createEvent('【' + target +  '】 Primary: ' + primaries[i] + ', Secondary: ' + secondaries[i],
                    new Date(endTime.getFullYear(), endTime.getMonth(), endTime.getDate() + 7 * i),
      new Date(endTime.getFullYear(), endTime.getMonth(), endTime.getDate() + 7 * (i + 1)));
  }
}

function upsertOncaller() {
  removeAfterNextWeekEvents('オンコール');
  addCalender('オンコール');
}

function upsertToiler() {
  removeAfterNextWeekEvents('トイラー');
  addCalender('トイラー');
}

function upsert() {
  upsertOncaller();
  upsertToiler();
}

Prometheusがアラートを検知

Prometheusにアラートを設定して、アラート発生時のアクションにWebhookを登録することで電話通知サービス(Sinatra製サービス)をキックします。
これらの設定はGrafanaのGUIから設定しています。

http://docs.grafana.org/alerting/notifications/ NewNotificationChannel

http://docs.grafana.org/alerting/rules/#notifications SetAlertNotification

Googleカレンダーに登録されている第1窓口の人に電話

キックされた電話通知サービスがGoogleカレンダーから窓口の人の名前を取得しに行き、名前から電話番号を引いてTwilioのAPIをコールします。
電話発信をした後、第1窓口の人が未対応という状態を保存します。

Googleカレンダーの取得

GoogleカレンダーをAPIで取得するにはOAuth2の認証が必要なので、電話通知サービスのデプロイ時に手動で認可しています。
(ここが自動化のネックになっているのでどうにかしたい・・・ナイスな案をお持ちの方は是非教えてください!)
アラートが全く鳴らない幸せな世界が続いた場合にリフレッシュトークンが切れるので何かしらのリカバリー手段が必要なのですが、幸い(?)アラートは少なくとも週1で発生しているのでまだ問題に直面していません _:(´ཀ`」 ∠):...

電話の発信

TwilioのAPIをコールしています。

TwilioのAPIは公式ドキュメントが充実しているので、そちらを参照して頂くのが良いと思います。
SDKのサポートしている言語の種類も豊富で困ることは少ないと思います(201809時点ではGoが非サポートです)。

公式の電話発信のRubyサンプルは以下のようになっています。

# Get twilio-ruby from twilio.com/docs/ruby/install
require 'twilio-ruby'

# Get your Account SID and Auth Token from twilio.com/console
account_sid = 'ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
auth_token = 'your_auth_token'

# Initialize Twilio Client
@client = Twilio::REST::Client.new(account_sid, auth_token)

@call = @client.calls.create(
  url: 'http://demo.twilio.com/docs/voice.xml',
  to: '+14155551212',
  from: '+15017122661'
)

puts @call.sid

オウチーノでは電話でアラートに気づけばいいよねということでサンプルボイスをそのまま流していますが、自前の音声ファイルを流したりテキストを読ませるということもTwilioではサポートしています。電話に出るのが楽しくなるようなアイデアを募集中です!

状態の保存

サービスのサーバのローカルファイルに状態を記載しています。
DBなどを利用するのがいいのでしょうが、手抜きしました v^^v

電話を受けた第1窓口はチャットボットに対応しますコマンドを投げる

チャットボット経由で未対応状態を解消します。

e.g. @ruboty ack

これはチャットボットがack(acknowledge)コマンドを受けたら電話通知サービスに対して状態削除APIをコールしています。
これは発信したユーザーを特に見ていないので、アラートに気づいて対応できる人がいたら代わりに投げてくれたりします。

ちなみにオウチーノではチャットボットにrubotyを利用しています。

約10分第1窓口の人から応答がなければ第2窓口の人に電話

cronで定期的に状態を確認し、アラートが発生してから10分間応答がない場合に第2窓口の人に電話します。
電話をかける方法は第1窓口の人に電話をした時と同じです。

今後の課題

途中にも書きましたがGoogleのAPIを呼ぶ周りをもう少し整理したいのと、現状アラートアクションのトリガーとなっているPrometheusと電話発信をするサービスの冗長性が皆無なので、どちらかが死ぬと悲劇が起きるため、何かしら対策を打ちたいと考えています。

最後に

モニタリングにオーナーシップを持つようになった結果、トラブルの予兆を見つけて事前に対策したり、リソースが余っている場合にはスケールダウンしてコスト削減したりといったことが柔軟にできるようになりました。
とはいえ手動な部分が多く残っているので、これからも改善を続けていきたいと思います。

オウチーノでは一緒に働くメンバーを募集中です

漸進的なシステムリプレイス(STTMeetup システムリプレイスNight)

オウチーノの吉川です。この度オウチーノでも開発者ブログをはじめます。 これからオウチーノの技術的な取り組みやサービス開発の裏側についてご紹介していきます。 どうぞよろしくお願いします。

システムリプレイスNight

先日STTMeetup#7 システムリプレイスNightが開催されました。 STTMeetupはスタートトゥデイテクノロジーズさんが定期的に開催しているミートアップで、今回はシステムリプレイスがテーマです。 スタートトゥデイテクノロジーズさんもちょうどZOZOTOWNのシステムリプレイスに取り組んでいるということで、 各社のシステムリプレイスに関する取り組みについて私と弊社山本がご紹介しました。

オウチーノのリニューアル

オウチーノでは昨年2017年に経営体制が変わり、この1年で開発体制も大きく変化しています。 システムのリニューアルや新サービスのローンチも着々と進んでおり、今月にはオウチーノのポータルサイトの中核である新築・中古サイトもリニューアルされました。

オウチーノはもともと新築、中古、賃貸サイトなどをそれぞれ独立したサービスとしてローンチし、あとから統合してきた経緯があります。 またもともと外注中心の体制だったため、担当業者によって技術スタックが異なっており、ColdFusion+OracleなものあればCakePHP+MySQLなものやStruts2もあったりとばらばらでした。それが今やReact+Railsのアプリケーションに集約されつつあります。

と、言葉にするのは簡単ですがもちろん容易なことではありません。システムのリプレイスはともすれば泥沼化しがちで、頓挫することもままあります。 そうならないようにどのような工夫をしてきたかについてSTTMeetupでお話しました。

分かれたシステムをていねいにモノリスに集約する

私からはリプレイスを進めるにあたっての方針策定やアーキテクチャ、組織的なとりくみについてご紹介しました。 タイトルだけ見るとMicroservicesつらいからモノリスにしたとか、分断されたモノリスの話を思い浮かべるかもしれませんが、そういった話ではありません。 ちなみにもとの状態を一言でいうなら分断されたモノリスが乱立した状態でした。

例えばPC版はColdFusionでスマートフォン版がPHPで同じOracleを見ていたり、バッチが何系統も別にあって、あるバッチが別のバッチの結果ファイルを監視してドミノ倒しで処理していくようなイメージです。

それ自体も良い状態ではないのですが、あれもこれも解決しようとしはじめるのは泥沼化への近道です。 オウチーノの場合はシステムの分断以上に開発者が分断されてしまっていたことが問題と考え、それを解決する手段としてモノリスを選択しました。

リプレイスではなくリノベーションという選択について

私からは新モノリスアプリケーションを中心お話しましたが、それを実現するための裏側として新システムと旧システムの間の構成について山本が発表しました。 新システムと旧システムの間をうまくつなげることで新システム側が開発しやすくなるだけではなく、新旧同じツールでデプロイやログ解析ができるようになりました。完全置き換えしなくてもちょっとした工夫でディレクターの待ちとエンジニアの作業を減らすことができ、ひいてはリプレイスがより進みやすくなります。 ちなみにタイトルはなんとかして不動産用語を使いたかったようです。

当日の様子

オフィスがものすごくおしゃれでケータリングも豪華でした。

f:id:adorechic:20180828194258j:plainf:id:adorechic:20180828195625j:plainf:id:adorechic:20180828201425j:plain

スタートトゥデイテクノロジーズのみなさんにこの場を借りて感謝いたします。 どうもありがとうございました。

最後に

これからのオウチーノを一緒に作ってくれるメンバーを募集中です。 少しでもご興味を持っていただけたならぜひご連絡ください。