くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログ。積極採用中です。

フルリモートワークでのオンボーディング施策

こんにちは。みんなのマーケットでCTOをしている戸澤です。

当社ではコロナ以前からリモートワークで働ける状態でした。 ただどちらかというと東京もしくは宮崎のオフィスで働くのが主流で、利用するメンバーはそこまでいませんでした。

3月下旬のリモートワークを会社全体で初めるタイミングでは、うまく仕事が回るのだろうか、という不安がありましたが、試行錯誤していく中でリモート環境下でも開発を回していく体制や仕組みを整えられてきました。 その結果、オフィスへの出社が不要なフルリモートでの採用も開始し、募集も日本全国、全世界に拡大しました。 その成果もあり、従来のオフィス出社の前提だと採用できなかったであろう鹿児島や韓国に住んでいるメンバーも入社しています。

今回はフルリモート環境下で入社したメンバーのオンボーディングをどう進めているかについて振り返っていきます。 フルリモートワーク以前から在籍しているメンバーはメンバー間の関係性や業務の基礎理解がすでに出来ている状態ですが、新しく入社するメンバーはそれがない状態で、直接会わずにリモートで進めるのは初めての試みでした。

昨日(2020/12/17)の東京の感染者は過去最多822人となりましたが、フルリモートでの転職を考えている方が働くに際しての安心できる材料として、また、フルリモートに悩んでいる組織運営者の助けとなれれば幸いです。

新卒入社メンバーに行ったこと

他メンバーが隣に座っている状態を再現する

新卒メンバーは4月1日の入社日からリモートでの研修、OJTとなりました。 まだ社会人としての働き方に慣れてない状態、業務の基礎がつかめてない状態で自宅にひとりになるフルリモートは懸念がありました。

研修は例年通り新卒向けのオンボーディング用のドキュメントに沿って進めますがリモートの状況だと、わからないところで手が止まってしまったり、新卒メンバーの会話の中で理解を深めることが難しい状況でした。

その対策として、新卒メンバーと指導メンバーでZoomを繋ぎっぱなしにしました。このZoomはいつもはミュートにしておきますが、ミュートを解除して話しかけると誰かが反応できる状態になります。これにより、新卒メンバー同士での解決や、新卒メンバーの疑問をすぐに指導メンバーに確認し解決することや、相談しやすい状況を作ることができました。

特に新卒メンバーのオンボーディングでありがちな、会話の中で確認を取っていくためテキストで表現が難しい内容や、遠慮して聞きづらいという状況を回避できました。 この施策は4月から8月まで行い、Zoom以外のビデオ通話サービスも試しましたが、ミーティングを開きっぱなしにしやすいことや、通話の品質や安定性でZoomが一番使いやすかったです。

中途入社メンバーに行ったこと

他メンバーが隣に座っている状態を再現する

中途メンバーは社会人として仕事を進められるためZoom繋ぎっぱなし施策は行っていませんが、次に施策を行っています。

  • ペアプログラミング
  • Slackにチームスレッドをつくり気軽に聞けるようにすること
  • Slackのテキストだと伝えづらいものはカジュアルにZoomやGoogle Meetを使い会話して解決すること
  • チューター制度
  • デイリーMTGの中で困っていることも話してもらい、早くに他メンバーがサポートに入れる状態を作ること

また、業務やスキル面以外でお互いにどんな人と働くか知りたいという意見もリモートワークを進めていく中で上がってきました。その方法として、全チームではありませんが、Zoomランチや、海外出身のメンバーも多いので出身の国について語ってもらう疑似海外旅行Zoomなどを開催することもあります。

内容の抜け漏れをなくす、検索できるようにする

4月以降、ドキュメントのカバレッジを上げていく中で、オンボーディング用のドキュメントの充実と更新を進めています。

オンボーディング用のドキュメントができる以前は、会話の中で教えていくことが多く、質問されないと伝え漏れること、時間がかかること、伝え間違えることがありました。そのため、開発していく中でのミスや認識違いによる手戻りが発生したほか、特にフルリモート下で組織を拡大していくに当たって、そのスケーラビリティが課題となっていました。

その対策として、開発フローやルール、システム、プロダクトの理解ができるオンボーディング用のドキュメントをエンジニア、デザイナー、QAの職種別に作成しました。作成後も入社のタイミングで不足している内容や変更になっている部分のアップデートも継続的に行っています。

その成果として、1回の入社人数が増えても対応出来ています。 また、部署配属から概ね1週間ほどで業務に取り掛かることができるようになり、事前の理解度が高まっていることや自分で調べることができる状態にもなっているため、進捗が途中で止まることが少なくなりました。

最後に

転職、就職しようとしている方は入社先で活躍できるか、心配されると思います。

その心配を解消でき早く活躍、成果をあげられるよう、これからもオンボーディングのハードルを下げ、新しいメンバーが業務やルールを早く理解できるように整備を続けていきたいと思います。

Work From Home #1

こんにちは、決済チームでバックエンドエンジニアをしております、 @akira です。

今回は、今年 3 月からリモートワークを開始した @akira が現在、

  • どのような環境で
  • どのように働いているか

をまとめた記事になります。

Work From Home は広義の意味でリモートワークと同義であり、以下では WFH と略称で記載しています。

Index

職場環境など

私は 2019 年 9 月にみんマに入社しており、前職では最大で週一回 WFH しているような状況でした。

弊社はフルフレックスのため、休憩一時間を除いて毎日平均 8 時間働ければ出勤・退勤のタイミングは問われません。

作業環境は、オフィスにあった Desktop(Ubuntu)を自宅に搬送し、ローカルで開発しています。

開発メンバーの中には、EC2 上に開発環境を構築している方もいます。

自宅のデスク

今のデスクは次のような状態です。

@akira's desk
普段はもので溢れかえっているデスク

上記アイテムは今年 3 月に入ってから買い揃えたのではなく、キーボード以外は事前に揃えていたものでした。1
それぞれの選択理由を以下に記載します。

机は前職の時にダイニングテーブルを購入していました。
子供の頃、学習机だとどこか違和感を感じ、リビングのダイニングテーブルではそれらの違和感がなく、恐ろしく作業が捗った経験があったことがきっかけです。

この違和感を深堀りしていくと「机の高さが合っていなかったのでは」との結論に達し、高さ 70cm のテーブルを調べていった結果、ダイニングテーブルにたどり着きました。Amazon の履歴が 1 年ほどしか保存されていないので詳細は失念してしまいましたが、おそらく価格は 5 万円前後です。

椅子

椅子は DXRACER の Racing モデルの赤で、オンラインゲーム用に 3~4 万円で購入しました。
3 年以上使っていますが、WFH でずっと座って仕事をするならばコスパはとてもよいと思います。

モニター

モニターは ASUS 27 インチのもので、かなり昔に 2~3 万円で買ったので今はおそらく廃盤になっています。
27 インチ程度あれば、作業をする上でおそらく困らない気がします。もっと大きな画面で作業すれば、また意見が変わってくるかもしれないです。

モニター台

モニター台は Amazon で数千円で購入したもので、上記の写真だとわかりづらいですが、モニター台直下にタコ足を配置するのが目的でした。

タコ足

タコ足も Amazon で 2~3 千円で購入したものです。
6 個口ですが、正直 8 個口くらいあっても良かった気もしています。

キーボード

キーボードは Magic Keyboard の US 配列です。以前は Bluetooth 対応の数千円の Mac 配列のものを使っていたのですが、TX(Typing Experience)が良くなかったので、少し前に Keychron K2 Gateron Brown Switch を購入して 1~2 ヶ月程使っていました。

Keychron K2 は Mac 配列の(Win 配列にも変更可能な)キーボードで、Gateron Brown Switch は高級感のあるような打ち心地で素晴らしかったです。

一方で、次第に私は

  • キーの”遊び”が少なく
  • 最低限の力で type できる
  • type sound は(自分自身が不快に感じなければ)あってもなくてもよい

キーボードが好みだと気づき、各種キーボードのスペックを 4 時間にらめっこした結果、最終的に Magic Keyboard に落ち着きました。

前職でも Magic Keyboard を使っていたため、その経験も大きく寄与しているかもしれません。

マウス

以前 PC で FPS(First Person Shooting)をしており、その際に RAZER DEATHADDER ELITE を約 1 万円で購入しました。

それよりも前は bluetooth のマウスを使っていたのですが、電池を格納しているためか、ちょっとしたカーソル移動も重く感じました。
その結果、有線のマウスに落ち着きました。

Laptop

Laptop は会社支給の Macbook Pro です。主に通話する際に使っています。
また、サブモニターとしても使っています。

Desktop

Desktop は会社支給の THIRDWAVE(OEM)に Ubuntu をインストールして使っています。
上記写真の右下に、床に直置きして使っています。

出勤から退勤まで

概ね次のようなスケジュール感で仕事しています。

  • 10:00 ~ 11:00 出勤
    • メールを見たり、ルーティン業務をこなしたり、Slack を眺めます
  • 12:00 チーム MTG
    • Zoom でチームミーティングを行います
      • 時間は特に決めていません
      • 今日やることと共有事項の二点を順番に話していきます
  • 13:00 ~ 15:00 のどこか 1h ランチ
    • 最近は(部署関係なく)社内メンバーを相手に、コーチングの練習を 1on1 で 30 分実施しています
      • コーチングを実施する側として、トレーニングメンバーに選出頂いたことがきっかけです
      • コミュニケーションの絶対量を稼げるため、非常にプラスになっています
  • 19:00 ~ 20:00 退勤
    • 出勤時間から逆算して、大体の目安で退勤します

上記よりも細かい粒度ですと

  • コミュニケーションは基本的に Slack のテキストベース、話したほうが早い内容は Slack call、さらに顔が見えたほうが良い内容は Zoom で行っています
    • このようなやり方で特に問題はないと個人的には思っています
  • 必要ならば、10 行以上の文章を Slack や GitHub 上で書くこともあります
    • 何かを共有したり教えたりするときに、背景も含めて全部説明すると大ボリュームになることがあります
      • 口頭でのコミュニケーションをそのまま文字に起こすと、これくらいの絶対量になるのかもしれません
  • 決済チームではほぼ雑談をしないため、ちょっとしたスキを見つけて雑談するようにしています
    • 仕事の話だけでは相手が今どのような状況(感情、体調 etc...)なのかが把握しずらいため、効果てきめんです

現在の WFH の課題

以下四点あると考えています。

  • テキストのミスコミュニケーション
  • 運動不足
  • 長時間座りつづけることによる身体的ダメージ
  • コミュニケーションのサイロ化

テキストでやりとりをしていると、同じ言葉でも context の違いによって意味が異なるケースがあります。
上述の「背景も含めて全部説明する」ことでもカバーできますが、口頭で会話したほうが正確で早いかもしれません。

運動不足については、リングフィットアドベンチャー を買おうか迷っているのですが、筋トレやランニングなどでも充分な気がしており、目下検討中です。

長時間座り続けることについては、スタンディングデスクの購入も検討しましたが、座ることのリスクをきちんと精査できていないため、一旦ストップしています。本当にまずいとわかった段階で購入予定です。

コミュニケーションのサイロ化については、オフィスで仕事をしていたときと比べ、部署の垣根を超えて話す機会が減ったと考えています。逆に今は Slack のテキストベースで仕事をしているので、業務に関係のない話を振りにくい、振られにくいのではないか、とも考えています。

実際に、色々なメンバーと話をしていても、自チーム以外のメンバーと話す機会がないので話したい、という声をよく聞くようになりました。

確かに出社していたときは、退勤後に話しながら駅まで歩いたりする空間などがありました。そういった小さな空間がアナログからデジタルに移行した際に淘汰されてしまったように感じています。

そこで(まだ 2 回しか開催できていませんが)社内で「すべらない話」を主催し、部署関係なくコミュニケーションできる機会を意図的に作っています。結果はノーコメントですが、目的は達成できているように思います。

WFH Tips

初歩的な内容なのであまり参考にはならないかもしれませんが、WFH のちょっとしたコツを紹介します。

  • Typing Speed を鍛える
  • Reading Speed を鍛える
  • 小休憩する
  • 音楽は流さない
  • 室内の CO2 濃度に注意する
  • 糖分補給

WFH ではよりテキストベースのコミュニケーションとなる旨を上述しましたが、テキストの input 方法は現在は typing が主流かと思います(将来は音声認識が主流となっているかもしれませんね)。 そのため、Typing Speed がテキストベースのコミュニケーションの Output Speed に直結します。 typing が遅いと自覚されている方は練習すると良いかもしれません。

私は大学時代にある小説にハマり、カフェに入って 6 時間かけて全部読み切るなど、活字を読むことに抵抗がありませんでした。最近は英語の文章や記事、論文を呼んでいて、英語の文章に対する抵抗感もほぼなくなりつつあります。このような経験が WFH におけるテキストのコミュニケーションスピードの向上に一役買っているように思います。

口頭でのコミュニケーションは synchronous であり、基本的にその場で理解する必要があるため、スピード自体にあまり差はないように思います(早口同士のコミュニケーションのスピードは早いかもしれませんが、業務において相手を選ぶことはできないでしょう)。

一方で、テキストでのコミュニケーションは asynchronous 2であり、その場で理解する必要がありません。そのため、スピード自体に差が出ます。この Reading Speed を上げていくことで、仕事全体をスピードアップできるはずです。

WFH によって、Slack のメンション以外は自分の作業を遮る事象は減ったように思います。そのため、1~3 時間ノンストップで作業していることもしばしばありました。

きりのいいところまで進めてしまったほうがもちろんよいのですが、全てを一度に片付けようとせず、必要に応じて目線を PC からそらして窓を見たりするなどの、ほんのちょっとの休憩は意識して取り入れるようにしています。

また、音楽を聞きながら仕事をされる方もいると思うのですが、個人的には必須ではなく、必要だと感じたときに音楽を流していました。

私は映画の soundtrack を流すことが多く、一人で戦争をしている状態が多かったのですが、これをやめました。

音楽によって作業効率が上がっている場合はよいのですが、逆効果に感じる場面もありました。よくよく考えてみれば、音楽を聞くことで生産性がプラスになる時間と、マイナスになる時間があってもおかしくはなく、効果は一様でないと今では考えています。

(私はやっていないのですが、)マリオカートの最終ラップで流れる BGM を流すことで作業の効率が高まる、などの説もあるようです。しかし、この手法は短時間のみ効果を発揮するのではないかと考察しています。

たしかにマリオの立場になって考えてみたら、最終ラップの BGM がゴールしてさらに 20 周した後でようやく止まると事前に知っていたら、その音楽が流れても全く焦りませんし、呑気にコースアウトしながら走っているかもしれませんね。

音楽を聞いている方で、作業効率が安定しない場合は、仕事中の音楽を完全にシャットアウトしてみるとよいかもしれません。

室内の CO2 濃度は、締め切った部屋で仕事をしていると高くなりがちです。CO2 濃度が低いほどパフォーマンスは上がるようですので、気がついたときに外の空気を吸うなどをしています。

最後に糖分ですが、糖分不足ですとやはり頭が回らないこともあります。その場合は積極的に糖分を補給しています。最近は HARIBO ゴールドベア にハマっています。

終わりに

上記を振り返ってみると、自宅であってもオフィスで仕事していても、仕事そのものには変わりがないという点はあらためて自覚しました。 工夫している点も、オフィスであっても実行できるものが多いですね。

ただし、WFH 特有の課題はあるため、今後はこちらを解決していって、より快適に仕事できるようにしていきます。

それでは次のブログでお会いしましょう!


  1. 業務外では主に Macbook Pro (2015 JIS, 2020 US)を使っていて、以前は Desktop(Ubuntu)も使っていました

  2. 日本語の文章内でも英語を使うことで英語の output をしているのですが、いつかみんマのルー大柴と呼ばれるその日まで続けていきます

コードをいじらずにPythonアプリケーションのメモリリークを検証する方法

こんにちは、バックエンドエンジニア・SREのカーキです。 最近くらしのマーケットのシステムで一部の Python アプリケーションにあったメモリリークを検証した時に学んだ検証方法について書きたいと思います。

メモリリークとは?

メモリリークはプログラムが確保したメモリを使用後に解放されず、プログラムのメモリ使用率がどんどん上がり続ける現象です。メモリリークがあると該当のプログラムがシステムのすべてのメモリを使い切って、システムがクラッシュする可能性があるので少し面倒なバグです。

リークの再現

弊社では現在Python 3.5.0を使っていますが、便宜のため以下のようにPython2系のdel関数の落とし穴を使ってメモリリークを再現します。

import time

class MyLeakyObject(object):
    def __init__(self, parent=None):
        self.parent = parent
        self.children = []
        self.value = 'x' * 100000

    def __del__(self):
        print("deleting")

def main():
    for i in range(2000):
        a = MyLeakyObject()
        a.children.append(MyLeakyObject(parent=a))
        time.sleep(3)

if __name__ == "__main__":
    main()

Python 3.4以降は上記__del__関数の落とし穴が解消されてるみたいです。

Python 2系でメモリリークを起こさないようにするには__del__関数使わない、または他のオブジェクトのレファレンスを持つ時にweakrefモジュールを使うのがおすすめです。

コードをいじらずにメモリリークを検証する方法

アプリケーションのコードにメモリプロファイラを入れて検証することが難しいケースもあると思って (例えばプロダクションでしか再現しないケースなど)、コードをいじらずにメモリリークの検証方法を調べたところ Pyrasiteという素晴らしいライブラリーを見つけました。Pyrasiteを使うと動いてるPythonのプロセスにコードを注入することができるので、Pyrasite + Pythonの好きなメモリプロファイラの組み合わせで簡単にメモリリークの検証ができます。

では、早速先程メモリリークを再現したコードで検証したいと思います。

1. Gdbをインストールする

Gdbunix系のシステムのデバッガーです。PyrasiteがGdbに依存してるのでインストールが必要です。インストール方法はOSによって違いますが、少し調べたら簡単にできるので割愛します。

2. Pyrasiteと好きなメモリプロファイラをインストールする

メモリ検証のツールとして今回Objgraphという便利なライブラリーを使います。

$ pip install pyrasite objgraph

3. Pyrasite shellを使ってプロファイラのコードを注入して検証する

まず、検証したいPythonプロセスのpidを調べます (以下の leak.py は上記のメモリリーク再現用のコードと同様のものです)

$ ps aux | grep leak.py
ec2-user 32497  0.0  0.0 132752  6224 pts/0    S+   04:46   0:00 /home/ec2-user/.pyenv/versions/2.7.10/bin/python leak.py
ec2-user 32541  0.0  0.0 119392   984 pts/4    S+   04:46   0:00 grep --color=auto leak.py

そして、pidを使ってpyrasiteのシェルに入ってプロファイラのコードを注入します。 今回はpyrasiteのシェルの中でobjgraphを使ってメモリリークの検証したいと思います。

$ pyrasite-shell 32497
Pyrasite Shell 2.0
Connected to '/home/ec2-user/.pyenv/versions/2.7.10/bin/python leak.py'
Python 2.7.10 (default, Nov 30 2020, 02:13:01)
[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(DistantInteractiveConsole)

>>> import objgraph
>>> objgraph.show_growth()
function                       1492     +1492
wrapper_descriptor             1050     +1050
builtin_function_or_method      724      +724
method_descriptor               590      +590
dict                            587      +587
tuple                           524      +524
weakref                         496      +496
list                            271      +271
getset_descriptor               222      +222
type                            214      +214

この段階でまだメモリリークしてることがはっきり見えませんが、数秒後にまた objgraph.show_growth()を実行してみると以下のような結果になります:

>>> objgraph.show_growth()
MyLeakyObject       16        +2
list               275        +2
dict               594        +2

メモリリークを再現したMyLeakyObjectクラスのオブジェクトがgcされずに増えてることがわかるかと思います。数秒後にまたチェックすると同じく増え続けます。

>>> objgraph.show_growth()
MyLeakyObject       18        +2
list               277        +2
dict               596        +2

....数秒後

>>> objgraph.show_growth()
MyLeakyObject       20        +2
list               279        +2
dict               598        +2

....数秒後

>>> objgraph.show_growth()
MyLeakyObject       24        +4
list               283        +4
dict               602        +4

これで明らかにMyLeakyObjectにどこかメモリリーク発生してることがわかるかと思います。

それでは、メモリリークを直してみましょう。__del__関数の落とし穴によってリークしていたので、__del__関数を削除するだけで直るはずです。

import time

class MyLeakyObject(object):
    def __init__(self, parent=None):
        self.parent = parent
        self.children = []
        self.value = 'x' * 100000 

    #def __del__(self):
    #    print("deleting")

def main():
    for i in range(2000):
        a = MyLeakyObject()
        a.children.append(MyLeakyObject(parent=a))
        time.sleep(3)

if __name__ == "__main__":
    main()

修正後に同じくpyrasite + objgraph で検証してみた結果は以下の通りです:

>>> import objgraph
>>> objgraph.show_growth()
function                       1491     +1491
wrapper_descriptor             1050     +1050
builtin_function_or_method      724      +724
method_descriptor               590      +590
dict                            577      +577
tuple                           524      +524
weakref                         496      +496
list                            261      +261
getset_descriptor               222      +222
type                            214      +214

>>> objgraph.show_growth()
wrapper_descriptor     1062       +12
getset_descriptor       226        +4
member_descriptor       214        +3
weakref                 499        +3
dict                    580        +3
method_descriptor       591        +1

>>> objgraph.show_growth()
>>> objgraph.show_growth()
>>> objgraph.show_growth()
>>> objgraph.show_growth()
>>> objgraph.show_growth()

ご覧の通り、objgraph.show_growth() を何回打ってもMyLeakyObject が上の方に上がって来なくなったことがわかるかと思います!

以上、コードをいじらずにPythonアプリケーションのメモリリークを検証する方法の紹介でした。 Pyrasiteobjgraph を使ってみて個人的にすごく便利だと思ったので、みなさんもぜひ機会があれば使ってみてください!

テストレベルとカバレッジとは!?

こんにちは、QAエンジニアのざきです。
冬といえば鍋ですね、好きな鍋は「もつ鍋」です。

f:id:curama-tech:20201119195257j:plain

はてさて
今日のブログは「テストレベル」と「カバレッジ」について解説します。
このブログを書くにあたり、英語を勉強している友人に聞いてみました。

Q1.テストレベルの意味って何だと思う?
A1.「どれだけちゃんとテストできているのかを示すレベル?」

Q2.カバレッジの意味って何だと思う?
A2.「え、知らない。聞いたことない。」

現場からは以上です。早速解説に入ります。

「テストレベル」とは
簡単に言うと、システム・ソフトウェアをテストするタイミングです。
※「テストフェーズ」や「テストステージ」とも呼びます。

開発手法により意味合いや取り扱いの範囲は変わりますが、代表的なテストレベルは以下の4つに分類されます。

f:id:curama-tech:20201119200921p:plain
●テストレベルの種類

単体テスト(UT)
 ユニットテスト[Unit Test]のテスト対象は、「コンポーネント」とも呼ばれるソフトウェア単体になります。
 テストの単位は、関数 / クラス / モジュール / コードなど、開発手法により異なりますが、ひとまとまりで動作する最小単位を指します。
 →くらマでは開発エンジニア自身で単体テストを行います。

結合テスト(IT)
 インテグレーションテスト[Integration Testing]のテスト対象は、2つ以上の「コンポーネント」の組み合わせです。
 また、「コンポーネント」と「サブシステム / データベース / 各種サービス」との組み合わせもテスト対象になります。
 結合テストの対象は、それぞれが単体テストを実施し欠陥が解消されている事が前提となります。
 →くらマでは結合テスト以降がQAチームの担当となります。

・総合テスト(ST)
 システムテスト[System Testing]のテスト対象は、全てのコンポーネントやサブシステムを統合した「システム全体」になります。
 全ての機能を統合した時に欠陥がないことを確認するため、実際の本番環境と同じ条件下、かつ、同様の使われ方を意識したテストを行います。
 もちろん、単体テストおよび結合テストを実施した上で欠陥が解消されている事が前提となります。

・受入/検収テスト(UAT, AT)
 (ユーザー)アクセプタンステスト[(User)Acceptance Test]のテスト対象は、総合テストを実施し検出した欠陥が解消された「システム全体」になります。
 受入/検収という名前の通り、大半は、開発の依頼元が納品前に行うテストとなり、実際に利用するユーザーがテストするケースも多いです。
 対象となるシステム・ソフトウェアが要求水準を満たしているかの検証試験や、利用者の意図通りに操作できるかの妥当性試験・ユーザビリティテストも行われます。

※上記は、あくまでも代表例として取り上げていますので、実際の開発現場や開発手法によって内容や取り扱いが変わる事があります。

テストレベルについての解説は以上です。そこまで難しい話ではないと思いますので覚えておいてもらえると嬉しいです ^^

カバレッジ」とは
テスト対象となるシステム・ソフトウェア全体のうち、どの程度のテストを実施した/実施しようとしているのかを割合(%)で表した「網羅率」を指します。
テストカバレッジ[Test Coverage]と呼ぶ事もありますが、大体は「カバレッジ」だけでも通じると個人的には感じています。

f:id:curama-tech:20201117213220p:plain
カバレッジの例

それぞれのカバレッジについて掘り下げると説明が長くなるので今回は省略しますが、計測する視点により様々なカバレッジが存在します。
テストを実施するにあたり、カバレッジ (網羅率)の測定/分析を行うことは、プロダクトの品質向上にとても大きな意味を持ちます。

以前も書きましたが、「全数テストを行う事は不可能」のテスト原則に基づき、計測視点別のカバレッジでテストを実施した/実施しようとしているのかを把握することで、テストの抜け・漏れの有無をチェックしやすくなります。
もしも過不足に気づいたら、そこから軌道修正する事でテストの妥当性を向上させる事もできます。

まとめ
①テストレベル(テストを行うタイミング)を理解しましょう。
無作為にテストするのではなく、テストレベルを把握して進めることで、後工程の出戻り(欠陥の検出・修正・再テスト、仕様変更等)を減らすことにも繋がります。

カバレッジ(網羅率:%)を意識しましょう。
開発されたシステム・ソフトウェアに係る全てをテストする事は不可能です。おさえるべき要点を掴み、カバレッジを読み取って、時には不要テストを切り捨てる覚悟を持ちましょう。

これからも、テストに関するネタを執筆していきます!
初歩的な内容にもなりますが、時に誰かの復習になったり、また時には誰かのテストに対する興味・関心を持っていただくキッカケになるかもしれません。
今回もお付き合いいただきありがとうございました!

くらマのオンライン決済を支えるデプロイ技術

こんにちは。 バックエンドエンジニア / SRE のまのめです。

くらしのマーケットではマイクロサービスアーキテクチャを一部採用しており、決済サービスも一つのマイクロサービスとして運用しています。
決済サービスは ECS で管理しており、コンテナは Fargate でコンピューティングしています。

f:id:curama-tech:20201110145722p:plain
構成の概略

ECS でのデプロイは、何も設定しないと Rolling update (徐々にリクエストを新しいインスタンスに流す) という戦略になります。
しかしデプロイ中に新旧のインスタンスが同時に存在すると、DB 構造やロジックの違いなどからデータが不整合を起こし、予期せぬ障害を招く恐れがあります。
そのため、新しいインスタンスに対してリクエストを一気に 100% 切り替える必要がありました。

さて、これを解決する方法は単純で、 Blue/Green デプロイができればリクエストを 100% 切り替える戦略が取れますね。
ECS でもこの戦略を取ることができ、インスタンスの切り替え方に様々な戦略を設定することが可能です。
この戦略は CodeDeploy で管理することができ、デフォルトで用意されている設定も数多くあります。
今回は、デフォルトで用意されている戦略の CodeDeployDefault.ECSAllAtOnce を利用して、Blue/Green デプロイを実装していきます。

実装

決済サービスをリリースする前から ECS を利用したサービスがいくつかあり、これらのデプロイは boto3 を利用しています。
なので、既存の仕組みに沿って実装していきます。
実はサービスとして CodeDeploy を使うのは初です。

注意点として、CodeDeploy による Blue/Green デプロイを実装するためには、Target Group を 2 つ用意する必要があります。
これは、古いインスタンスとは別の Target Group を新しいインスタンスに紐づけて healthcheck を行い、問題なければデプロイを実行して切り替える、という流れになるためです。

class CodeDeployManager:
    def __init__(self, cluster: str, service: str):
        session = Session(
            aws_access_key_id="YOUR_AWS_ACCESS_KEY_ID",
            aws_secret_access_key="YOUR_AWS_SECRET_ACCESS_KEY",
            region_name="ap-northeast-1")
        self.client = session.client("codedeploy")
        self.waiter = self.client.get_waiter('deployment_successful')
        self.application_name = "AppEcs-{0}-{1}".format(cluster, service)

        self.application = dict()
        self.application["applicationName"] = self.application_name
        self.application["computePlatform"] = "ECS"

        self.deployment_group = dict()

    def get_or_create_application(self):
        """
        :return response: dict
        """
        get_param = dict()
        get_param["applicationName"] = self.application_name

        try:
            self.application = self.client.get_application(**get_param)
        except (self.client.exceptions.ApplicationDoesNotExistException):
            self.application = self.client.create_application(**self.application)

    def get_or_create_deployment_group(self, **kwargs):
        """
        :param kwargs: dict

        require:
          - deploymentGroupName
          - serviceRoleArn
          - deploymentStyle
          - ecsServices
          - loadBalancerInfo
        """

        param = dict()
        param["applicationName"] = self.application_name
        param["deploymentGroupName"] = kwargs["deploymentGroupName"]

        try:
            self.deployment_group = self.client.get_deployment_group(**param)
        except (self.client.exceptions.DeploymentGroupDoesNotExistException):
            self.deployment_group = param
            self.deployment_group["applicationName"] = self.application_name
            self.deployment_group["deploymentGroupName"] = kwargs["deploymentGroupName"]
            self.deployment_group["serviceRoleArn"] = kwargs["serviceRoleArn"]
            self.deployment_group["deploymentStyle"] = kwargs["deploymentStyle"]
            self.deployment_group["blueGreenDeploymentConfiguration"] = {
                "terminateBlueInstancesOnDeploymentSuccess": {
                    "action": "TERMINATE",
                    "terminationWaitTimeInMinutes": 2
                },
                "deploymentReadyOption": {
                    "actionOnTimeout": "CONTINUE_DEPLOYMENT",
                    "waitTimeInMinutes": 0
                }
            }
            self.deployment_group["ecsServices"] = kwargs["ecsServices"]
            self.deployment_group["loadBalancerInfo"] = kwargs["loadBalancerInfo"]
            self.client.create_deployment_group(**self.deployment_group)

    def create_deployment(self, **kwargs):
        """
        :param kwargs: dict

        require:
        - deploymentGroupName
        - revision
        """
        param = dict()
        param["applicationName"] = self.application_name
        param["deploymentGroupName"] = kwargs["deploymentGroupName"]
        param["deploymentConfigName"] = "CodeDeployDefault.ECSAllAtOnce"
        param["revision"] = kwargs["revision"]

        deployment = self.client.create_deployment(**param)
        self.waiter.wait(deploymentId=deployment["deploymentId"])

今回の場合、要件として以下のように設定しています。

  • Blue/Green デプロイ時に、100% リクエストを切り替える
param["deploymentConfigName"] = "CodeDeployDefault.ECSAllAtOnce"
self.deployment_group["blueGreenDeploymentConfiguration"] = {
    "terminateBlueInstancesOnDeploymentSuccess": {
        "action": "TERMINATE",
        "terminationWaitTimeInMinutes": 2
    },
}
  • 全てのデプロイが完了するまで待機する (Slack 通知などのため)
deployment = self.client.create_deployment(**param)
self.waiter.wait(deploymentId=deployment["deploymentId"])

細かくは端折りますが、あとはこの CodeDeployManager を使って、デプロイの流れを作ります。

class DeployManager():
    def __init__(self):
        self.ecs = EcsManager()          # ECS の操作・設定
        self.params = ParameterManager() # デプロイの詳細設定

    def deploy(self):
        # 実装は書かないが、ここで ALB, Task, Role などの詳細設定を済ませておく

        self.code_deploy = CodeDeployManager(self.ecs.["cluster"], self.ecs["service"])
        self.code_deploy.get_or_create_application()
        self.code_deploy.get_or_create_deployment_group(**self.params.deployment_group)
        
        if service:
            # update service のときは、deployment を作ることで update できる
            self.code_deploy.create_deployment(**self.params.deployment)
        else:
            # create service のときは、deployment と deployment group を作るだけ
            self.ecs.create_service(**self.params.ecs)

デプロイを開始すると、CodeDeploy 上に Deployment が作成され、CodeDeploy による Update Service が始まります。

f:id:curama-tech:20201110145917p:plain
デプロイ中の様子

ECS の画面でも、状態が確認できます。

f:id:curama-tech:20201110145727p:plain
ECS での Deployment の表示

感想

思っていたよりも遥かに簡単に実装できました。
もちろん、より細かい要件を定めている場合は Deployment Group の設定などが複雑になったり、場合によっては Deployment config を自作するということもあるかと思います。
デフォルトで用意されている config だけでも十分な実装が可能ですので、 ECS でトラフィック管理をしっかりやりたい場合は CodeDeploy によるデプロイ戦略を作るのが重要だなと感じました。

今後も決済に関するアップデートは行われていきます。
アップデートをより安全にデプロイしていくために、デプロイ戦略もアップデートできたらいいなと思います!