RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

在宅勤務での取り組み&在宅でのナレッジ

こんにちは、CTOに続き一年ぶりとなりましたレアジョブとディズニーをこよなく愛している岩堀です。
基本弊社は在宅勤務になり、ディズニーも休園が続いて、家からほとんど出ることもないので、 仕事中もディズニーの音楽かけて夢の国にいる気分で仕事をしています。 皆さんはどのように在宅勤務をこなしていますかね?

今回は弊社での在宅勤務の取り組みについてと、ちょっとした在宅に役立つナレッジを書きたいと思います。

社内の取り組み

弊社は緊急事態宣言を前に全社にて在宅勤務へ移行しました。 また弊社の海外子会社は日本よりも先にロックダウンが発生したことにより、 強制的に在宅勤務に移ることになりました。 準備もできていないうちに在宅勤務に移ったことで社内システムを担当する ITソリューションチームでは現在も一丸となって対応しております。 それでは在宅勤務が行われる前と後でどのように変わったか、少し書きたいと思います。

在宅勤務前の状況

弊社はオフィス勤務がメインでした。各ツールに対してもオフィスからのIPに制限しておりました。 外部にいるときもVPNを経由して、各ツールへアクセスを行っておりましたので、 外部からアクセスが必要な社員に対してのみVPNの設定を行っておりました。 これは海外の子会社でも同様の対応で行っておりました。

在宅勤務前
before environment

在宅勤務後の状況

在宅勤務後は基本的にオフィスでの業務がなくなりますので、すべての従業員がVPNでつなぐ状況となりました。

在宅勤務後
after environment
急遽在宅勤務の環境へ移行したことで問題となってきたのは2点ありました。

  • VPNライセンス数
  • VPN機器のリソース負荷

弊社ではVPNとしてCiscoのAnyConnectを使っており、ライセンス数は利用されるユーザー数分必要になります。
元々全従業員がVPNへ接続することを想定していなかったので、全従業員数分のライセンスの購入は行わず、機器の最大接続数分のみを購入しておりました。
ライセンスの追加購入を考えていたところ、CiscoのほうでCovid-19による在宅勤務への移行をサポートするように、 期間限定のライセンスを無償でプログラム提供しているのを知り、こちらを利用してライセンス数をクリアしようとしております。

www.cisco.com

またVPN機器のリソースは定期的に監視を行っており、負荷状況が高い状態であるので、こちらに関しては何かしらの対応を行っていこうと思います。 現状、VPNを利用する前提として、Web会議はVPN機器のリソースだけでなく通信トラフィックも無駄に使ってしまうため、VPNを経由しないようにしていただくことと、VPNが不必要な状態のときには切ってもらうように周知しておりますが、やはり会議で必要な資料を開く上でVPNが必要な場合もあるので、VPN接続し続けた上で行われることがあるようです。このあたりの回避できる方法を考えていかなければないですね。 

在宅勤務でのナレッジ

在宅勤務へ移行したことで、リモートにてヘルプデスクとして数々の問い合わせいただく中で、 在宅で勤務をする上で家での環境が整っていない状況が数多くあるということを知りました。 そこで、現在の環境でも快適な物にできるようにちょっとしたナレッジを書きたいと思います。

通信環境

今回、このような状況になって知ったのですが、現在自宅にネット回線を引いていない方が結構いるということを知りました。 その方たちは在宅で仕事する際にはスマホテザリングなどで対応しているようですが、通信容量が多くなって通信制限にかかってしまう方がいらっしゃるようです。 そこで在宅ワークにおいてテザリングを使う中で通信容量を抑える方法をあげたいと思います。

動画やWeb会議などはあまり利用しないのは当然となりますが、業務でどうしても必要な状況があります。 その場合にはVPNは経由しないほうが通信容量は抑えることができます。 VPN通信には通常の通信と比べて暗号化等を行っているため付加情報がついており、通常よりも通信容量が増えます。 検証のため自分の端末を使って、みんな大好きなミニーのショー動画(2分半)を見ながら試してみました。

www.tokyodisneyresort.jp

テザリング端末での通信容量チェッカーアプリを使った大雑把な値ではありますが、VPNなしのほうが10MB程度節約になりました。

通信環境 通信容量
VPNなし 29MB
VPNあり 42MB

実際Web会議をされるときは長時間となると思いますので、可能な限りVPNは経由しないほうが、 通信容量を抑えることができます。

またブラウザを開いているだけで通信が行われることもあるので、PCからしばらく離れる場合には テザリングをOFFにして通信を行わないようにしておくことでチリツモを防げますので大事かと思います。

作業環境

オフィスではラップトップの他にディスプレイも割り当てられて、拡張ディスプレイを使って、 業務をやられている方も結構いるかと思いますが、在宅勤務でも同じように行う上で、ディスプレイの購入をされた方もいるかと思います。
私も買おうかと思いましたが、置くスペースもないので、家にあるものでどうにかならないか考え、テレビにHDMIケーブルを使って外部ディスプレイとして使うことを考えました。

ただガジェット好きな私の家では既にHDMIポートをフルに使っており、毎回抜き差しするのも面倒と思ってしました。 そこで思いついたのが、FireStickTvをAirPlayとして使ってできないか。

早速やってみたところあっさり行うことができました。

ケーブルもなくスッキリした状態で作業ができるので、結構便利です。 やり方は以下の手順で実施しました。

  1. FireStickTVにAirPlay用アプリをインストールする
    私は評判の良かったAirReceiverを入れました。

  2. AirReceiverでAirPlayをOnにする

  3. MacにてAirPlay検索より該当のディスプレイを選択(この場合はAFTT-26)

  4. 拡張ディスプレイにするために、「個別のディスプレイとして使用」を選択

これであっという間に拡張ディスプレイとして機能します。ちなみに裏で別アプリを動かすことも可能なので、AmazonMusicをかけながら拡張ディスプレイとして使うことができるようになります。

私のマシンはMacなのでAirPlayでやりましたが、Windowsの場合はMiracastで同じことができます。 こちらはアプリをインストールすることなくFireStickTVのミラーリング機能で対応可能です。
こちらについては別の機会で記載したいと思います。

ぜひ皆さんも快適な在宅勤務をして、このコロナに負けずに頑張りましょう!

国境を超えてスクラムを実践する

こんにちは。一年ぶりの登場となりますCTOの山田です。

さて、RareJob Tech Blogも無事一年が経ち、当初の宣言通り週1ペースで様々な記事が投稿されてきました。また、この一年、レアジョブは様々な変化がありました。中でもレアジョブ英会話のシステムリニューアルの意思決定をしたことが開発部門にとって大きな変化です。背景や目的としていることなどはRareJob Appealの記事をご覧下さい。

このシステムリニューアルPJでは、日本だけではなくフィリピン開発部隊との連携が必要となります。レアジョブは、連結子会社6社ありますが開発部門を持っているのが日本のレアジョブとフィリピン ケソン市にオフィスを構えるRareJob Philippines, Inc.の2社だけですので、この2社間の連携が非常に重要です。この2社間でどのような体制、開発プロセスでシステムリニューアルPJを進めるのかを紹介したいと思います。 国を跨り協働開発する際の参考になればと思います。なお、RareJob Philippines, Inc.ではレアジョブレッスンを提供する講師に関わるすべてのシステム開発・運用を担っております。

開発組織間の連携

リニューアルプロジェクトでは、多くの不確実性と変化が伴うためスクラムで開発を推進していきます。アジャイル開発は経験主義を基本としているので、いきなり大きなプロジェクトを立ち上げてもうまく行きません。普段からどのような仕組みでコミュニケーションを取っているかで立ち上がり方が違います。言語、文化、時差など様々な違いをどう吸収するかを考えながらこれまで組織を構築してきました。現在では各レイヤーでコミュニケーションを密に取り、レアジョブ英会話サービスを共に支える組織となりました。
日本とフィリピン共に機能組織としての技術本部があり、ほぼ同じような組織構造で構成しております。

体制イメージ
各レイヤー間の連携

  1. Engineer Manager間での情報共有
    RareJob Philippines, Inc.の開発責任者へは週次で会社の動きを共有。特に日本を軸に計画が大きく変わることがあるため、影響ある情報をいち早く伝えることを心がけています。
    また、Engineering Manager間は月一で情報共有会を開催。プロジェクトの動きや課題の議論などをこの場で行ないます。もちろん必要であれば随時オンラインミーティングを開催しています。

  2. Lesson Roomの協働開発
    Skypeに変わるWebRTCを利用したレッスン提供機能であるLesson Roomを生徒側は日本、講師側はフィリピンで開発を行っております。
    コアとなるロジックを共通化しているため、密に連携しながら生産性を向上させる取り組みをしております。

  3. 生徒向け、講師向けに関わる開発、リリース情報の共有
    基本的には双方のサービス要件に沿ったドメスティックな案件が多いです。ただし、現在は共通のDBを利用して生徒、講師システムが動いておりお互い影響し合うため、リリース情報の共有は会議やチャットなどで行ないます。システムリニューアルPJでは、この共通DBをどのように分割するかを議論しています。

  4. レアジョブグループの共通基盤協働開発
    Golangで開発されたプロダクト共通の情報資産を有する共通基盤。各プロダクトからはAPIで利用可能で、レアジョブ英会話サービスでは、リニューアルプロジェクトを通じて一部の機能を共通基盤へ移管していきます。この共通基盤は、生徒システム、講師システム双方で利用するため、協働で開発しております。

  5. AWSの協働運営
    レアジョブでは、複数のプロダクト、メディア、社内向けのサブシステムなど多くのシステムが稼働しています。講師の給与計算などフィリピン固有の重要なシステムもあります。これらを安定的に運営していくため、ルールや権限など厳格に設計し、協働で運営してます。

システムリニューアルPJの進め方

プロトタイプとしてMVPを創る

レアジョブ英会話も気がつけば多くのお客様に利用されている規模の大きなシステムとなりました。この規模のシステムリプレイスをいきなり進めるには、多くの手戻りが発生することが容易に想像でき、またビジネスへの影響も広範囲に渡るため、2019/11〜2020/3まではプロトタイプという位置づけでレアジョブ英会話のMVPを開発しました。
このプロトタイプフェーズでは、MVPの開発だけではなく以下も行ないました。

国境を超えてスクラムを実践する

プロトタイプ開発での経験を経て、この時の最終的な仕組みをベースに、フルバージョンの開発は下記のような整理をしました。

体制

  • 4つのスクラムチームから構成(日本とフィリピンの混同チームもある)
  • 全体の意思決定者であるプロダクトオーナーがおり、各スクラムチームにチーム毎の意思決定をするサブプロダクトオーナーを配置
  • スクラムチームに技術的な意思決定をするテックリードを配置
  • 全チームのプロダクトオーナー、テックリードからなるステアリングコミッティを設置

Global TimelineとPBL

  • 開発効率観点から開発していく順をEpic単位で定義したGlobal Timelineを策定し、ステアリングコミッティで全体の進捗を管理する
  • ユーザーストーリーを一元管理し、PBLに分解し各スクラムチームで開発を担う

スプリントとスクラムイベント

  • 期間は2週間で、開始、終了をすべてのチームで合わせ、スクラムイベントを同日に一斉開催する
  • スクラムチームでのプランニング前に全体プランニングとして、次スプリントで必要となるPBLを他チームへ依頼する
  • スクラムチームの振り返り後、全体での振り返りを実施

全体像

システムリニューアルPJの進め方全体像
システムリニューアルPJの進め方全体像

その他の取り組み

  • 共通の課題リストを持ち、PJ全体で解決に向けた取り組みを行なう
  • マイクロサービスとは言え、原則の技術選定に準ずる
  • 運用が複雑化しないようにCI/CD、監視などの仕組みを統一していく
  • 利用するツール(ドキュメンテーション、コミュニケーションツールなど)を統一する
  • 原則英語でドキュメントを書き、分散させない
  • 各自英会話スキルをレアジョブ英会話で磨く
  • 定期的に出張しFace to Faceで理解を深める(新型コロナ問題で当面厳しそう)
  • 背景、目的、方針は何度も同じことを伝える
  • 技術顧問の広木さんに、移行方法をレビューして頂く(第三者がレビューすることで気づきが多い)

最後に

システムリニューアルPJでは、モノリシックアーキテクチャからマイクロサービスアーキテクチャへとシステムそのものを変えていきます。これだけでも難易度が高いですが、フィリピン開発部隊と進めていくことでさらに難易度が上がります。
また長期プロジェクトになるため、問題が発覚したらすぐに解決に向けて動く必要があり常に神経を尖らせて、関係者と密な連携を取り進めていきます。 このプロジェクトを通じてレアジョブグループ全体の開発機能が一段高いレベルに上がることを確信しており、またこれから立ち上がる新規事業へも貢献出来ると思います。

ということで、また来年(!?)お会いしましょう。

電動式昇降デスクのすゝめ

コロナの影響で世間が騒がしい中、皆さんお元気ですか。ジャンボです。

弊社でもリモートが標準となり、これまでリモートをしてこなかったメンバーも新しいワークスタイルに戸惑いつつも新しいメリットなどに気付き始めている頃かと思います。

今日はですね、そんなリモートを支える技術・・・いや、ツールとして「電動式昇降デスク」という概念を紹介したいと思います。

自分のための机を探す

prtimes.jp

昔はデスクの高さを調整したり、調整用の補助器具を買ってみたこともあるんですがやはりしっくり来ず・・・ いろいろ見ていた時にIkeaショールームで出会ったのが昇降式のデスクという概念でした。いろいろ見ていると電動ではなく、手動の昇降式デスクという商品も多く、検討したんですがハンドル式の調整バーなどをぐんぐん回して高さを30cm前後毎回調整することになり、これは結構疲れる作業なので電動式のものを導入しました。

私の持っているデスクはすでに廃盤になってしまったのですが、大体の電動式昇降デスクは高さを記録することができ、 私だと以下のようにシチュエーション別に高さを調整しています。

  1. 座り仕事用モード(75cm
  2. 立って仕事する用モード(110cm
  3. コード読む用モード(80cm

一般的なデスク高は70cmとされており、これと比べると1, 3は少し高く設定しています。 ただ私は名前の通りジャンボなので、身長188cmの体には一般規格はちとやりづらく腰を痛めることも多々ありました。

最適なデスクの高さを決める要素

一般的にデスクの高さを決めるには以下の3つの要素があるとされています。

  1. 身長
  2. 椅子の高さ
  3. 椅子の高さとデスクの高さの差異

またデスクの高さは1により求められるとされ

デスクの高さ(cm) = {身長(cm) * 0.25} - 1 + {身長(cm)* 0.183} - 1

という計算式が存在します、私の場合だと 79cm となります。 標準と比べて10cmほど差があることがわかります、他の要素でも調整は可能で一概には言えないですが、この差異をストレスに感じることも少なくなく・・・ これが姿勢の悪化を生んでいた気がします。

電動でなくとも、昇降式でなくとも、自分のために作業環境をしっかり計測して揃えるだけでパフォーマンスは大きく変わると思います。

参考 https://www.isunokoujyou.com/hpgen/HPB/entries/59.html

使いこなすために

2年ほど使ってみて、今では自宅作業時にはコーヒーと並んで欠かせない存在となっています。☕️ 椅子やPCと同じかそれ以上にパフォーマンスに響くものだと思うので、この機会に導入を一考ください。

モードの使い分け

「立って仕事する用モード」は主に朝方や食後に集中できない時に使っています、仮眠を取るより気持ちを切り替える方が効果的に感じる体質なので重宝しています。 「コード読む用モード」は普段より視線を少しあげて延々コードレビューしたり、OSS読んだりする時に使います。位置高めであんまキーボード使わない時ですね。

これらを使い分けることで、作業に応じて環境と気分をスイッチングするのが楽しいです。

時間を区切って使う

長時間立っているのはもちろん健康上の負担がないわけではありません、そのため立ち作業をする場合はある程度時間幅を決めたり、タスクで区切ったりして終わりをちゃんと設定します。 また腰の負担を軽減するために寄りかかる手段や、高めのスツールがあるとよりGoodです。 私は椅子のヘッドレストがちょうどよくこれに寄りかかっています。

実は音が出る

また音に関しても注意が必要で、まったくの静音というわけではなく多少唸ります。 音もモーター音なので多少ノイズに感じる人もおり、寝てる人とか猫ちゃんはびっくりするかもしれません。

私的には「さーて、いっちょやっちゃいますか」という時に作動音とともに環境がセットアップするのはめちゃくちゃアガるので好きです。

コード類を整理する

高さ調節時にモニターのケーブルが動いて絡まってガチャガチャすることがありました、ケーブル類はデスク裏に格納することをお勧めします。もし天板を別途用意するのであればケーブルを通す穴を持っていることをお勧めします。

ケーブルハックであれば Guild の go ando さんの記事がおすすめです。

note.com

まとめ

いろいろ書きましたが、やはり電動式昇降デスクはいいです。

立っても座っても仕事ができるし、なにより立って仕事ができます。

この記事を書いていて、なぜ立って仕事をすることがいいのか?ということを改めて考えてみるとやっぱり「"早く座りたい"という気持ちが仕事を早く終わらせる」というのが一番大きいなと改めて思いました。

今週もお疲れ様でした。

USM(User Story Mapping)をやってみた

はじめに

お久しぶりです。改善したいマンの三上です。
世間は新型ウィルスで混乱していますが、レアジョブのブログは元気に更新中です!
私事ですが、少し前に丸2日間の研修を経てLSM(Licensed Scrum Master)の資格を取得しました。

レアジョブではアジャイル開発を推進しており、各チームやプロジェクトで積極的に採用されています。
そんな中で弊社のメインプロダクトである「レアジョブ英会話」のリプレース案件を実施することになりました。
この超大型案件を進めるにあたり、まず始めに何から始めたらいいのか考えていく中で
まずはプロトタイプ開発をしよう!スクラムでやってみよう!
という運びになり、まずはユーザーストーリーマッピング(USM)をやってみたので
今回はその記事を書こうと思います。

USMとは?

スクラム開発においてプロダクトの価値を決めるのはプロダクトバックログです。
プロダクトバックログはいくつかのPBI(プロダクトバックログアイテム)が優先度の高いものから順番で並んでいます。
PBIはユーザーストーリー形式になっていて、誰にとって、どんな価値があるのか表現されている必要があります。

ユーザーストーリーは以下の形式にします。

WHO = ○○として
WANT = △△したい
WHY = なぜなら✗✗だからだ

実際に例を上げると、

WHO = レアジョブ英会話の会員として
WANT = 日本語も話せる講師とレッスンしたい
WHY = なぜなら分からない時に日本語で質問したいからだ

のように書きます。
上記は例ですが、ユーザーストーリーの粒度は大小様々になりえます。
このようなユーザーストーリーを優先度が高く直近のスプリントで着手するものほど、より詳細なストーリーにしていく必要があります。

(ちなみにストーリーの詳細化・見積もりの事をリファインメントと呼びます。)

USMは上記のようないくつかのユーザーストーリーで構成されます。
そのユーザーストーリーをナラティブフローと呼ばれる物語の流れに沿って左から右へ時系列順に並べていきます。

USMを作成するメリットはプロダクトの全体像を俯瞰して見れることです。
個人的に、この複数のサブストーリーを経て、全体としてのストーリーが出来上がる感じが
RPG感があって楽しいです。笑

というわけでやってみた

f:id:rarejobmikami:20200323024258j:plain

ざっくり以下のような形で進めました。

Timebox

3時間

Agenda

  1. 目的説明
  2. ユーザーストーリーマッピング説明
  3. 現状の機能概要のおさらい
  4. ペルソナの共有
  5. ユーザーのアクションを書き出す(全員)
     時系列に並べていく。
     同じ目的はグループにまとめる。
     注:ストーリーとして書く。〇〇として△△したい
     注:ストーリーをタスクに分解してはならない
     「青い付箋」にユーザーのアクションを書き出す
     「オレンジ付箋」にアクションのグループをまとめる
  6. 改善ポイントや欲しい機能を書き出す
     「赤い付箋に」ユーザーの気持ちになって欲しい機能を書き出す
  7. 整理(優先順に並び替える)
  8. 最優先で作りたいものを決める(MVPの確定)
     MVP(Minimum Viable Product)とは、まず最初にリリースしたいものになります。
     今回はMUST(必ず必要)、WANT(出来れば欲しい)の2つに分類し、MUSTをMVPとします。
     ※余裕あれば次に達成したいことを決める
  9. プロダクトバックログに組み込む

参加者は開発チームはもちろんPO(プロダクトオーナー)に当たる企画チーム、CTOまでバラエティに富んだメンバーで行いました。

完成したUSMはこちら

f:id:rarejobmikami:20200326204520j:plain

オレンジ色の付箋がエピックと呼ばれる単位になります。
例としては「ユーザー登録」、「検索」、「レッスン予約」のような粒度です。

また上部に貼ってある付箋ほど優先度が高いものになります。

やってみて良かったこと

大きく以下の2つ。

・ペルソナを明確にした
・プロトタイプ開発のスコープが明確だった

です。これは企画チームに感謝です。

上記の写真でプロジェクターに映し出しているのが、
レアジョブ英会話の重要ペルソナを言語化したものになります。
ここが明確だと何が良いかというと、ユーザーストーリーを考えるにあたって
最も重要なのがユーザー視点に立つことです。

これは簡単なようで難しいです。なぜなら人それぞれ求めることが違うので当然意見が分かれるときがあります。
またエンジニアあるあるだと思うのですが、
パッと要件を聞いた時に、すぐ実装方法を考えてデータ構造とかアーキテクチャとかを想像しちゃうと思うんです。

それはそれで重要なんですが、今回の場では不要です!

そんな時に役に立つのがペルソナの存在です。
意見が別れたり悩んだときには、ペルソナの気持ちになってその都度振り返ることが出来て、円滑に議論を進めることが出来ます。

また、プロトタイプ開発のスコープが明確だったことにより、どこまでやる・やらないの判断にあまり時間を取られずに進行することが出来ました。

最後に

このUSMを経て、プロダクトバックログを作成し、無事にスプリントが開始することができました。
またスプリント開始時にやったことについても次の記事で書きたいと思います。

それでは、また。

マイクロサービスのロギングベストプラクティスとGoの実装の場合

こんにちは、プラットフォームチームの池田と申します。初投稿です。

プラットフォームチームではマイクロサービスアーキテクチャの構成を採用し開発を進めています。
どんな構成でも忘れてはいけないのがロギング。いわゆる非機能要件の1つで地味な存在ですが、サービス運用を支える上で非常に重要です。

直近でマイクロサービスにおけるロギングの構成を調査し、プラットフォームチームでメインで採用しているGo言語での実装を検証しました。
今回の記事ではそのまとめを紹介します。

目次

ロギングベストプラクティス for マイクロサービス

マイクロサービスにおけるロギングの方針に関して記載している日本語の記事が少なく感じたので、はじめに調べた結果のまとめを記載します。

リクエストにユニークなIDを付与し紐付けができるようにする

マイクロサービスでは、あるサービスAがサービスBを呼びさらにサービスCを呼ぶといった形になるので、呼び出しチェーンにユニークなIDを与えることで調査の見通しが良くなります。

このとき、アプリケーションがHTTPレスポンスなどでエラーを返す場合にもリクエストのユニークIDを入れると良いようです。そうすることで、問題が発生し際ユーザが受け取ったエラーとユニークIDを素早く紐付けて調査を開始することができます。

ユニークなIDをどこでどのように生成かということも重要なポイントでしょう。原則としてはユニークIDでの追跡範囲のエントリーポイントとなる箇所で生成します。そしてそういった箇所で利用されるロードバランサのサービス(AWSではELB)やKongなどのAPI Gatewayミドルウェアプラグイン的にCorrelation ID(ユニークID)生成の機能を提供しているので、それを利用するのが一般的なようです。そういった機能では、生成したユニークIDをオリジナルのHTTPヘッダーへ挿入します。

ログは一箇所に集める

上述での各サービスが出力するユニークID付きのログを横断的に調査するために、各サービスのログを一箇所に集中させることが次に重要になります。

このとき、アプリケーションがPush型として能動的にHTTPリクエストなどを使って集約場所へ登録するのではなく、ローカルストレージのファイルやAmazon Elastic File Systemといったクラウドのストレージに一旦預けた後に、LogstashFluentdといったツールで集約場所へ連携することが望ましいとのことです。そうすることで、アプリケーションからログ集約という役割を切り離すことができます。

以下の図はここまでの2つのポイントを踏まえた構成の一例です。Amazon ELBがCorrelation ID(ユニークID)を生成しマイクロサービスサービス間で伝搬され、出力されたログはCloudWatch Logs Subscriptionの機能でElasticsearch Serviceへと集約させています。

f:id:ochataro:20200316183408p:plain

ログデータを構造化する

マイクロサービスではログに持たせるフィールドは柔軟にしておきたい一方で、サービス共通でロギングデータのパースができるようにもしておきたいです。

そこでサービス共通でログデータのフォーマットを合わせましょう。JSONやLTSVといった構造化の形式を統一させることで持たせるフィールドも柔軟になり、共通で必須なフィールドを容易にパースすることができます。

ログに有益な情報を持たせる

マイクロサービスアーキテクチャにおいてログ情報として持つことが望ましいフィールドが以下になります。

どのサービスでも共通で持つのが望ましいフィールド

リクエストのエントリーポイントとなるサービスで持つのが望ましいフィールド

  • リクエスト元のIPアドレス
  • ユーザが利用したブラウザ名またはモバイルのOS名
  • HTTPレスポンスコード

Go言語での実装例

上述のベストプラクティスの内容を踏まえて、Go言語の場合の共通ロギングライブラリとアプリケーションのサンプルを作っていきます。

はじめに、重要なのは具体的にどういったフィールドやフォーマット、出力先にするかをチーム間で議論し合意した上でそのルールを決めることでしょう。

ここでは、ロギングルールを決定できているという前提で進めていきます。

今回の記事ではソースコードすべて載せられていませんが、後述のサンプルAPIサーバを実行し、下記のようにユニークIDを想定したHTTPヘッダー付きでリクエストすると、

curl -H 'X-Transaction-ID:00A-abcdef-99B' http://service-hostname/v2/sample

APIサーバから以下のようなJSON形式のログが標準出力へ出力されます。(フォーマッティングは後付しました。)

{
   "level":"error",
   "msg":"SearchSample panic!",
   "request-id":"00A-abcdef-99B",
   "service-name":"MicroService01",
   "stack":"goroutine 6 [running]...スタックトレースが続く...",
   "time":"2020-03-15T23:39:58+09:00"
}

マイクロサービス共通で利用するロギングライブラリ

上述したマイクロサービスのロギングの共通ルールは共通ライブラリとしてサービス(チーム)間で共有されるべきです。

package logger

import (
    "github.com/sirupsen/logrus"
)

const (
    // XTransactionID はユニークIDのためのHTTPヘッダーのキー名です。
    XTransactionID = "X-Transaction-ID"
)

var (
    // Log は本パッケージのグローバルインスタンスです。
    Log = defaultLogger()

    // ServiceName は各アプリケーションのビルド時に ldflags を利用して埋め込まれます。
    // 下記はビルドの例です。
    // go build -ldflags "-X path/microservice-logging/logger.ServiceName=MicroService01"
    ServiceName = "not-set"
)

// Logger は公開インターフェースです。
type Logger interface {
    Debug(xTxID interface{}, msg string, fields ...Field)
    Info(xTxID interface{}, msg string, fields ...Field)
    Error(xTxID interface{}, msg string, fields ...Field)
}

// NewLogger は Logger インターフェースのコンストラクタです。
// 基本として、マイクロサービスのアプリケーションはこれを利用せずに Log インスタンスを利用することがルールです。
// 開発をする場合や調査をする場合などで Config を設定しこのメソッドを呼び出す。
func NewLogger(conf *Config) Logger {
    return newLogger(conf)
}

func defaultLogger() Logger {
    return newLogger(NewConfig()) //NewConfig() で共通ルールに基づくデフォルト設定がされる。
}

type logger struct {
    *logrus.Logger
    config *Config
}

func newLogger(config *Config) Logger {
    var l = logrus.New()
    {
        l.Level, _ = logrus.ParseLevel(config.minLevel.String())
        l.Formatter = config.formatter
        l.Out = config.out
    }
    return &logger{
        Logger: l,
        config: config,
    }
}

func (l *logger) Debug(xTxID interface{}, msg string, fields ...Field) {
    ...
}

func (l *logger) Info(xTxID interface{}, msg string, fields ...Field) {
    ...
}

func (l *logger) Error(xTxID interface{}, msg string, fields ...Field) {
    ...
}

上記の共通ロギングライブラリにおいて実装のポイントをピックアップすると以下になります。

  • Goのロギングのライブラリには現時点(2020年3月時点)で、最も多くのGitHubスター数のあるlogrusを利用
  • グローバル変数(Log)をエクスポートしそれだけを利用させるようにしている
  • ログのフィールドに入れるサービス名(ServiceName)はアプリケーションのビルド時に組み込むようにしている
  • インターフェースのメソッドの引数にユニークID(XTransactionID)を指定する

これらのポイントはあくまでも一例に過ぎません。どのように実装するかはチームでのコーディングポリシーなどに依存するでしょう。

続いて以下はロギング設定のコードです。デフォルト設定はマイクロサービスのルールとして決定した内容になります。

package logger

import (
    "io"
    "os"
)

// Config は Logger 設定の構造体です。
//
// formatter: Format type of logging, TEXT or JSON
// out:       io.Writer of the logger output
// minLevel:  Minimum level to out
type Config struct {
    formatter Formatter
    out       io.Writer
    minLevel  Level
}

// ConfigOption はFunctional Options Patternで
// Configのフィールドを設定するための関数型です。
type ConfigOption func(*Config)

// NewConfig は *Config のコンストラクタです。
//
// マイクロサービスのルールである各デフォルト設定
// Formatter: JSON
// Out:       STD OUT
// MinLevel:  Info
func NewConfig(options ...ConfigOption) *Config {
    config := &Config{
        formatter: Formatters.JSON,
        out:       os.Stdout,
        minLevel: Levels.Info,
    }
    for _, option := range options {
        option(config)
    }
    return config
}

func WithMinLevel(minLevel Level) ConfigOption {
    return func(c *Config) {
        c.minLevel = minLevel
    }
}

func WithFormatter(formatter Formatter) ConfigOption {
    return func(c *Config) {
        c.formatter = formatter
    }
}

func WithOut(out io.Writer) ConfigOption {
    return func(c *Config) {
        c.out = out
    }
}

上記の例では

  • ログデータはJSON形式 (formatter: Formatters.JSON)
  • ログの出力先は標準出力 (out: os.Stdout)

というルールをデフォルト設定としています。(脱線ですがGoのio.Writer, io.Readerはとても良いものです。)

これらの設定に関してもチームが持つインフラ構成や方針に影響されるものです。

共通ロギングライブラリを利用するアプリケーションコード

上記の共通ロギングライブラリを利用するアプリケーションの実装例は以下のようになります。

ここではGinフレームワークを利用したAPIサーバを想定しています。Ginのミドルウェア内でプログラムpanic時にスタックトレース付きでロギングします。

import (
    "fmt"
    "net/http"
    "runtime"

    "github.com/gin-gonic/gin"

    "path/microservice-logging/logger"
)

var (
    defaultStackSize = 4 << 10 // 4 kb
)

// Recovery はGinフレームワーク で利用するミドルウェアです。
// 下層からの panic をリカバリーしロギングとHTTPレスポンスセットを実施します。
func Recovery() gin.HandlerFunc {
    return func(c *gin.Context) {
        defer func() {
            if r := recover(); r != nil {
                err, ok := r.(error)
                if !ok {
                    err = fmt.Errorf("%v", r)
                }
                var (
                    stack  = make([]byte, defaultStackSize)
                    length = runtime.Stack(stack, true)
                )
                logger.Log.Error(
                    /* Request-ID */ c.Request.Header.Get(logger.XTransactionID),
                    /* msg */ err.Error(),
                    /* Additional fields */ logger.F("stack", fmt.Sprintf("%s ", stack[:length])),
                )
                c.AbortWithStatusJSON(http.StatusInternalServerError, &gin.H{
                    "errors": []string{"unexpected error"},
                })
            }
        }()
        c.Next()
    }
}

func main() {
    router := gin.New()
    {
        router.Use(
            Recovery(),
        )
    }
    ...
}

おわりに

ベストプラクティスな方針を知ることができましたが、どうやって実現するかはどうしても開発チームごとに工夫しながら作っていく必要があると思います。日々精進です💪

また、今回のロギング実装の例ではパフォーマンスに関する考慮が不足しています。

  • メモリアロケーションがより効率的なzapというライブラリに切り替える
  • リクエストをさばいた最後にまとめて出力するようバッファを利用する

といった改善案が考えられます。
今後こういった面でも検証する予定です。

それでは (^_^)/~

参考

Nuxtで環境に応じた環境変数をいい感じに設定する

APP/UXチームに所属しております、フロントエンドエンジニアの田原です。
前回の記事から時間が空きましたが、弊社ブログ2回目の登場です。よろしくお願いします。

(隙あらば自分語り) 私は映画が好きなのでよく映画館に観に行くのですが、
最近はコロナの影響もあり外出を極力控えているので、そうこうしている間に観たい映画の劇場公開が終わってしまうのだろうなという寂しさで胸がいっぱいになる日々を過ごしております。 (直近ではミッドサマーを観に行こうと思っていましたが、、、諦めました)

ちなみに誰も興味がないとは思うのですが一番好きな映画はインファナル・アフェア - Wikipedia(3部作)とダークナイト - Wikipediaです。(一番と言っておきながら4本も上げていますが、どうしても順位がつけられないので)

インファナル・アフェア好き!という方と中々巡り合った事がないので、これを機に好きになる方が増えると嬉しいです。

さて、前置きが長くなってしましたが、今回の本題に入らせて頂きたいと思います。
過去の記事でも紹介しておりますが弊社ではフロントエンドフレームワークにVue.js(以下、Vue)を採用し、開発をおこなっております。

Vueを使った開発経験ある点もあって、最近、新たにスタートしたProjectにおいてはNuxt.js(以下、Nuxt)を採用する運びとなりました。
※前回の記事は↓↓ rarejob-tech-dept.hatenablog.com

今回はNuxtで開発をおこなっていくにあたり、先に設定しておくと楽だなと思ったProject内での環境変数の取り扱いについてご紹介させて頂きます。

目次

環境変数の扱いについて

Nuxtを利用してProject(App)の準備を行うにあたり、create-nuxt-appコマンドで環境の準備をおこないますが、その際に.envファイルが自動で生成されるので、 このファイル内でApp内で利用する環境変数を定義し読み込めるようにすることが多いと思います。

とはいえ、開発環境が多岐にわたってくると(local/dev/prod)初期設定時からの.envファイルのみでは扱いが少し面倒になってくるかと思います。

実現したかった事

  • 環境毎対応する.envファイルを定義しておき、Nuxt Appのdeployやbuildのscriptコマンドに応じて、それぞれの環境変数がApp内で参照出来るようにしたい。
  • 環境毎の設定については可読性や拡張性についても考慮した作りとしたい。
  • SSR対応のProject(App)ではない為、クライアントサイドからの参照ができれば良い。

対応について

1. dotenv(@nuxtjs/dotenv)をProject Appに追加

create-nuxt-appで生成したApp(Project)に対して、dotenv(@nuxtjs/dotenv)を追加します。 github.com

※dotenvとは カレントディレクトリに置かれた.envファイルを読み込み、そこに記述されたキー&バリューのペアをprocess.env 経由で参照できるようするnpm moduleです。 Nuxtで生成されたAppに対しては以下のコマンドでnuxt専用のものを追加します。

yarn add --dev @ nuxtjs / dotenv #またはnpm install --save-dev @ nuxtjs / dotenv
2. nuxt.config.jsに設定を記載

追加したdotenv moduleを利用する為にnuxt.config.jsのmoduleの項目に@nuxtjs/dotenvを追記します。

nuxt.config.js

︙
modules: [
  '@nuxtjs/dotenv'
 ︙
]
3. 各環境に対応するenvファイルを設定

Project Rootに各環境毎に対応する.envファイルを設置します。

config/

config
├── .env.dev
├── .env.local
└── .env.prod

各々の.envファイルにApp内で利用したい環境変数を記載します。

e.g. env.local

BASE_API_ENDPOINT_URL="https://hogehoge/local"

e.g. env.dev

BASE_API_ENDPOINT_URL="https://hogehoge/dev"

e.g. env.prod

BASE_API_ENDPOINT_URL="https://hogehoge/prod"

※変数は複数定義してOK

4. package.jsonのscriptsでENVに各々の環境の文字列を渡すように指定する

package.json

"scripts": {
    "dev": "ENV=local HOST=0.0.0.0 PORT=3000 nuxt",
    "build-local": "ENV=local nuxt build",
    "build-dev": "ENV=dev nuxt build",
    "build-prod": "ENV=prod nuxt build",
  },

※ENVに.envとして指定したファイル名(今回の例ではlocal/dev/prod)を渡している。

5. nuxt.config.jsに設定を追加し、config/配下の.envファイルが読み込まれるように対応する

nuxt.config.js

const envPath = `config/.env.${process.env.ENV}||'local'`
require('dotenv').config({ path: envPath })


export default {
  ︙
  dotenv: {
    filename: envPath
  }
}

【補足説明】 * 4で設定したscriptsでENVに文字列を渡している為、nuxt.config.js内においてprocess.env.〇〇で文字列を取得できる。 * envPathに各.envファイルへのPathが代入され(config/.env.local等)、これによりdotenvを使って3で定義した各.envファイルへの参照が可能に * dotenvのpathにenvPathを指定する。(ファイル名をオーバーライドする為の設定)

6. 型エラー解消

1〜5までの対応が終わっていれば、App内(.vue/.ts等)の各ファイルからprocess.env.〇〇で.envファイルに定義した環境変数に対して参照を行うことができます。

︙
created() {
  process.env.BASE_API_ENDPOINT_URL // scriptsで渡した引数の環境に応じた値が参照可能
}

ただし、process.env.〇〇という形ではApp内のどこからでも呼べてしまうのは統一性が無い&参照の際のprocess.envに型を入れたいので型の指定とplugin化を以下の様に行います。(型指定を行わない場合、Nuxt側で持っているdefaultの型がprocess.envにあたる為、想定した型で参照できない場合があります)

// environments.ts等(plugin file)

export type export type EnvironmentVariables = {
  // nuxt default property ------------
  NODE_ENV: string
  browser: boolean
  client: boolean
  mode: 'spa' | 'universal'
  modern: boolean
  server: boolean
  static: boolean
  APP_NAME: string
  // nuxt custom propety --------------
  BASE_API_ENDPOINT_URL: string
}

export cont environments: EnvironmentVariables = {
  // nuxt default property ------------
  NODE_ENV: process.env.NODE_ENV!,
  browser: process.browser!,
  client: process.client!,
  mode: process.mode!,
  modern: process.modern!,
  server: process.server!,
  static: process.static!,
  APP_NAME: process.env.APP_NAME!,
  // nuxt custom propety --------------
  BASE_API_ENDPOINT_URL: process.env.BASE_API_ENDPOINT_URL!
}

export defalut (context: any, inject:(name: string, v: any) => any) => {
  inject('environments', environments)
}

nuxt.config.js

︙
plugins: ['@/plugins/environments']

準備したplugin file内で適当な名前でinjectしnuxt.config.jsにもpluginの宣言を行います。 また、TypeScript側で環境変数が設定されていることを検知させるためにtypes/enviroments.d.tsを設定します。

import Vue from 'vue'
import { EnvironmentVariables } from '@/plugins/environments'

declare module 'vue/types/vue' {
  interface Vue {
    $environments: EnvironmentVariables
  }
}

最終的にApp内でthis.$enviroments.process.env.〇〇という様に指定した環境変数を参照することができます。

参考にさせて頂いたサイト

dotenvについて
process.envの型指定について

まとめ

APIに対してのRequest basePathが環境によって異なることに気づいたタイミングでこの対応を行いましたが、最初にこの辺りについて対応しておいた方が楽だったなぁっと思い、備忘録も兼ねて内容を残しました。

editor formatのチーム間の設定やaxios-modulesとの付き合い方やstoreの設計、その他にもCSS-moduleやstorybookの設定周辺等、Nuxtで環境を作っていく上でこんな感じで作っていくといいかも思える様な設定などについても、改めてご紹介できればと思っております。

最後まで読んで頂きありがとうございました。

Ansible の cron モジュールで環境差分を減らしてみる

ご無沙汰してます

先日、大好きな女性アーティストである aiko の楽曲がサブスクリプションで提供されて狂喜乱舞しております DevOps チームのおくさんです
好きな aiko の曲は「ジェット」の再録音 ver. です
これまでライブには 3 回程しか行けていませんが、一度もライブで「ジェット」を聴いたことはないです・・・

さて、前回記事を書いてから約半年経過し、久々の執筆となります

前回は AWS の CloudWatch に関する記事を書きましたが、今回は Ansible の利用において便利だなと思った「cron モジュール」について書いてみようと思います

rarejob-tech-dept.hatenablog.com

目次

Ansible の cron モジュール

Ansible では、サーバやネットワーク機器等の設定に関する構成管理を行うことができますが、行う操作によってはモジュールという形で提供されています
たとえば、yum でパッケージをインストールするには「yum モジュール」を利用して各種パッケージのインストール等を行います

「cron モジュール」は文字通り cron 設定を管理するためのモジュールで、以下のドキュメントに利用方法が記載されています
こちらのモジュールを利用することにより、cron 設定の管理を Ansible にて行うことが可能になります

docs.ansible.com

やってみよう

cron モジュールのドキュメントに Examples がありますので、これを参考にしつつ動かしてみましょう

今回はサンプルとして、以下のような Playbook を作成してみました
設定内容は、毎日 0:00 にroot ユーザで date コマンドを実行するものとなります

test_cron.yml

---
- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'

なお、今回利用する Ansible のバージョンは以下です

/work/ansible # ansible --version
ansible 2.9.2

では、作成した Playbook を実行してみましょう

/work/ansible # ansible-playbook -i inventory.ini test_cron.yml

PLAY [test] ********************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************
ok: [x.x.x.x]

TASK [Test] ********************************************************************************************************************************************************************************************************
changed: [x.x.x.x]

PLAY RECAP *********************************************************************************************************************************************************************************************************
x.x.x.x              : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

サーバの crontab を確認すると、無事に root ユーザで cron の設定が行われたことを確認できます

[root@x.x.x.x ~]# crontab -l -u root
#Ansible: Test
0 0 * * * date

環境にあわせて cron の有効無効を変更する

さて、本題です

Ansible を利用する際には、本番環境や開発環境でなるべく同じ Playbook を利用したい場合があると思います
しかし、「本番環境では cron を動かしたいが、開発環境では cron を動かしたくない」といったように、環境によって行うべき挙動に差が生まれることも多々あると思います

その場合、cron モジュールにおいては、「disabled パラメータ」を利用することにより環境の差分を吸収することが可能です
disabled パラメータはドキュメントに以下のように記載されています

disabled 
boolean

Choices:
no ←
yes

If the job should be disabled (commented out) in the crontab.
Only has effect if state=present.

したがって、本番環境で cron を有効にする場合は「disabled: no」、開発環境で cron を無効にする場合は「disabled: yes」とすることにより、開発環境のみ cron のコメントアウトを行うことができます
これにより、同一の Playbook を利用して環境によって挙動を変更することができます

せっかくなので、上述で利用した Playbook を変更して試してみましょう
本番環境で cron を動かしたいときは以下のような Task になります

- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'
        disabled: 'no'

また、開発環境で cron を動かしたくないときは、以下のような Task になります

- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'
        disabled: 'yes'

違いは disabled パラメータだけであり、このままだと冗長になってしまうので、変数を利用して切り替えましょう
変数を利用することにより、1 つのタスクで動作の切り替えが可能になります

- hosts: test
  become: yes
  tasks:
    - name: Test
      cron:
        name: 'Test'
        minute: '0'
        hour: '0'
        day: "*"
        month: "*"
        weekday: "*"
        job: 'date'
        state: 'present'
        user: 'root'
        disabled: "{{ DISABLED }}"

それでは、変更した Task を利用して Playbook を実行してみましょう

今回はサンプルなので、extra-vars でサクッと変数に値を代入して Playbook を実行します
まずは DISABLED 変数に yes を代入します

/work/ansible # ansible-playbook -i inventory.ini test_cron.yml -e DISABLED=yes

PLAY [test] ********************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************
ok: [x.x.x.x]

TASK [Test] ********************************************************************************************************************************************************************************************************
changed: [x.x.x.x]

PLAY RECAP *********************************************************************************************************************************************************************************************************
x.x.x.x              : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

実行した結果、cron をコメントアウトができました

[root@x.x.x.x ~]# crontab -l -u root
#Ansible: Test
#0 0 * * * date
[root@x.x.x.x ~]#

今度は、DISABLED 変数に no を代入します

/work/ansible # ansible-playbook -i inventory.ini test_cron.yml -e DISABLED=no

PLAY [test] ********************************************************************************************************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************************************************************************************
ok: [x.x.x.x]

TASK [Test] ********************************************************************************************************************************************************************************************************
changed: [x.x.x.x]

PLAY RECAP *********************************************************************************************************************************************************************************************************
x.x.x.x              : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

実行した結果、cron 有効にすることができました

[root@x.x.x.x ~]# crontab -l -u root
#Ansible: Test
0 0 * * * date

まとめ

ジョブについては各種 OSSクラウドベンダーのサービスを利用して管理することも多いと思いますが、まだまだ各サーバ内で cron として実行している環境も多いと思います
Ansible を利用することでも cron の管理は可能ですので、 この記事がどなたかのお役に立ちますと幸いです

また、Ansible で提供されているモジュールは非常に便利なので、各種ドキュメントをしっかり読みながら、今後もいろいろ探して導入検証をしてみようと思います