selmertsxの素振り日記

ひたすら日々の素振り内容を書き続けるだけの日記

アジャイルとリーン・スタートアップを組み合わせた開発プロセス ~第1回 概要~

2019年8月にAzitに入社して4ヶ月。 私はSREとしての役割を期待されてAzitに入社したけれども、気がつけばバックエンドエンジニア兼スクラムマスターをやっていました。

バックエンドエンジニアとしては、AWSインフラ環境の完全な作り直しとTerraformによるコード化、Railsの負債解消、監視設定のコード化などを行っていました。 スクラムマスターとしては、初期の3ヶ月はアジャイル開発(スクラム、XPを組み合わせたもの)、そして12月からの1ヶ月はリーン・スタートアップ開発の導入等を行いました。

ここでは、スクラムマスターとして考えた開発プロセスについて資料にまとめます。 なおこの文章は、0-1 の開発フェーズではなく、すでにリリースされたサービスに途中で加わったスクラムマスターの目線で書かれており、対象とする読者も私と同じような境遇にあるスクラムマスターとなっています。

この開発プロセスは市谷先生の「正しいものを正しくつくる」という書籍をベースに、手を加えたものであることを最初に伝えておきます。 私が市谷先生の本を誤って解釈している部分があれば、コメントを貰えると嬉しいです!!!

最初に

Lean Startup、XP、スクラム、Kanban、DevOps、SRE、Lean Analytics、デザインスプリント。 プロダクトを開発していく上で、先人達が考え出した、様々な考え方、フレームワーク、方法論があります。

それらの手法を把握し、確実に守って運用できていれば、必ずプロダクトを成功させられるわけではないことをエンジニアの人ならよくご存知でしょう。 結局の所、私達はそのときそのときに、真剣に課題に向き合い、適切な学習を継続し、最適と思われる意思決定を積み重ねるしかありません。 その意思決定の深さ、観点、施策実行の精度、そしてときには運によって、プロダクトの成否は決まります。

それでも、プロダクトを開発していく中で、知ってさえいれば防ぐことができた問題。 無駄にせずに済んだ時間。避けることが出来た衝突があるはずです。 私はプロジェクトを成功させるために、様々な本を読み込みました。 それらの知識とこれまでの経験を、この記事に残そうと思います。

TL;DR

  • サービス開発は、リーン・スタートアップ、デザイン思考、アジャイル開発 ( スクラム、eXtream Programing 以降 XP )を組み合わせて行う
  • リーン・スタートアップを用いて、追うべきKPIの設定や検証すべき仮設を設計し
  • デザイン思考を用いて、仮説を検証するため何を作るべきか考え
  • アジャイル開発を用いて、デザイン思考で得られた施策を形にしていく

背景

アジャイル開発 ( スクラム + XP )

ベンチャー界隈において、アジャイル開発という手法は有名です。 そのなかでもスクラムは、軽量であり導入しやすいため最もよく利用されています1

スクラムを導入すれば、開発物の内容や優先度、届けたい価値について、PBI(Product Backlog Item)という形で可視化されます。 また、このスプリントで何をしなければならないか、誰が何に取り組んでいるかもSBI(Sprint Backlog Item)やカンバンによって可視化されます。 誰が何をやっているのか、どの程度進捗しており、どこで詰まっているのか可視化するこの手法によって、 チームは優先度が高い課題に集中して取り組み、協力し合いながら機能的に活動できるようになります

スクラムは様々な職種にまたがって開発していくための軽量な開発手法であり、ソフトウェアエンジニアとしての技術的な制約をその中に含めません。 そのため、開発チームはXPのエッセンスを取り入れ、それらを共通認識として開発することがあります2。 一般的によく取り入れられるXPのエッセンスの中には、TDDやペアプログラミングリファクタリング、自動テスト、インクリメンタルなリリースなどが挙げられます。 これらの取り組みについてはすでに一般的になっており、特別に意図せずともXPの取り組みを行っていたという開発チームは珍しくはないでしょう。

XPとスクラムどちらか一方を選ぶようなものではなく、共存させて運用する ものだと私は考えています。 そのため、スクラムをやっている方も、XPの概念について学んでみると色々と発見があるのでおすすめです。

リーン・スタートアップ

スクラムにおいて、ビジネスと開発チームをつなげる役割はプロダクトオーナーが担っています。 「アジャイルエンタープライズ3では、プロダクトオーナーには下記の役割があると記載されています。

プロダクトオーナー(PO)は、顧客価値を考慮しながら、作業の特定と優先順位付けに責任を持ちます。 また、プロダクトの進化に合わせて顧客からのインプットとフィードバックを取り込み、顧客価値を向上させていくことに責任を持ちます。 (中略)... POは顧客の代弁者であり、顧客が本当に必要としている方向にプロダクトを進めるために、顧客フィードバックを取得します。

また、プロダクトオーナーは顧客価値だけでなく、事業に利益をもたらすことの責任も持ちます。 つまり ビジネスとプロダクトをつなぎ、顧客価値を最大化させつつも、事業に貢献する責任を持つのがプロダクトオーナーの役割と言えます。 顧客価値に責任を持つプロダクトオーナーと、ビジネスに責任を持つビジネスオーナーと役割を2つに分けている会社もあります。 スクラム開発におけるプロダクトオーナーの責任をどのように分担するにせよ、誰かが何らかの判断基準に基づいて開発物を決める必要があります。 スクラムガイドラインにおいて、プロダクトオーナーの持つ責任は明確に定義されているものの、遂行するための具体的な方法については触れられていません。

プロダクトオーナーはプロダクトバックログの内容や優先度という形で、開発したいサービスの機能や、それぞれの機能の重要度についてメンバーに伝えます。 とはいえ、プロダクトオーナーも何が正解なのか、把握している訳ではありません。 「正しいものを正しくつくる」4 において、この問題は下記のように説明されています。

現在の私たちが手がけるプロダクトづくりは、誰かの頭の中に正解イメージがあってそれをうまく取り出してコードにしていくという開発ではない、ということだ。 (中略...) つまり、いま私たちが作ろうとしているプロダクトとは、「どうあるべきか本当のところ誰にもわからないが、なんとかして形に仕立てていく」、そういう開発になる。

プロダクトオーナー1人にそういった「不確実性」に向き合う役割をもたせるチームは少なくないでしょう。 事業がうまく行っているときは、プロダクトオーナー1人が仮説立案、及び意思決定をしても問題が起きることはそうそうありません。 けれども、事業が上手くいかなくなったとき、1人だけで適切な意思決定をし続けることができる人間は多くありません。 小さくともリターンが得られるリスクの低い選択肢を選びがちです。 そして事業はじわじわと後退し、メンバーはプロダクトオーナーの実力に疑いを持ちはじめ、チームは徐々にまとまりを失っていきます。

私としては、チーム全体でそれらの「不確実性」に向き合っていくべきと考えています。 もちろん、最後の意思決定はプロダクトオーナーが行います。 しかし、検証すべき仮説の立案、及び何を作るかについてはチーム全体で考えます。 リーン・スタートアップ開発は、チーム全体で、検証すべき仮説を作成し、検証、学習の一連のプロセス実施していくための方法を提供してくれます。 そのため、私達はリーン・スタートアップ開発を取り入れて開発をすることにしました。

私は、リーン・スタートアップの開発プロセス全体については「図解リーン・スタートアップ成長戦略」5を、 施策の評価方法については「Lean Analytics」6を参考にして、開発プロセスを設計しました。

デザインスプリント

私は、人は基本的に「アイデア」と「アイデアを発案した人間」を分けて評価することができないと考えています。 権威ある人間による的を外したアイデアと、権威のない人間による素晴らしいアイデアであれば、選ばれやすいのは前者です。 恐ろしく口がうまい人間は、アイデアの魅力を実際の何倍にも膨らませ、現実歪曲空間を作ることもあります。 アイデアをアイデア自身ではなく、発言した人間や話し方によって評価してしまうことは、サービス開発において一般的によくあることです。 特にあなた自身がスクラムマスターやリーダーである場合、あなた自身の意見にチームメンバーが誘導されてしまい、 アイデアの多様性が損なわれるリスクがあることを理解する必要があります。

良いアイデアを提案者によらずに正しく評価し、事業に役立てなければなりません。 そうしなければ事業を成長させることはできませんし、さまざまな観点で物事を捉えることのできない、視野狭窄で不健全なチームになってしまいます。 先入観にとらわれずに良いアイデアを考案し、発案者のバイアスを取り除いて選出し、事業に役立てるためには、そのための適切なプロセスが必要となります。 私は、そのプロセスを「SPRINT」7というデザインスプリントについて書かれている書籍を元に設計しました。

開発フローの全体像

f:id:selmertsx:20191222203844p:plain
開発プロセスの全体像

開発プロセスの全体像を上の図に示します。「正しいものを正しくつくる」のP.8に書かれている図を元に私たちのチームに合わせて修正しました。 違いは、事業計画の一部も開発プロセスの中に取り込んでいる点です。 その理由は、1プロダクト1ベンチャーの小さいチームにおいては、開発チームも事業計画に関わり、その方向性に影響を与え、 そこで決まった内容は開発チームの中に過不足なく共通認識を作られなければならないからです。

次の記事において、開発プロセスの詳細について記載していきます。

参考資料

エンジニア面接において構造化面接を取り入れる際に色々考えたときのメモ

前提

このドキュメントは、Work Rules失敗の科学 、そして Google re:Workに記載されている面接方法をベースに書いています。上記の資料について、既に古い認識になっている。またはより良い知識がある場合は、このドキュメントは既に古いものになっている可能性があります。

構造化面接とは

構造化面接とは、同じ職務に応募している応募者に対して、同じ面接方法を使って評価しようという取り組みです。 すべての応募者に同じ質問をして、同じ尺度で回答を採点し、事前に定めた評価基準に基づいて採用を決定します。

構造化面接を採用する理由

私達エンジニアは面接のプロという訳ではありません。 また、基本的にエンジニアはアピールがうまい訳ではなく、相手の良いところや、技術的な強みを見つけ出すのは面接官の力量に掛かっています。 面接に専門性を持たない我々が直感に従って面接をすれば、相手の良いところを見つけ出すことが出来ずに、誤った意思決定をしてしまうことでしょう。 そのため、面接官の力量に強く依存せずに、網羅的に相手の良さを見つけ出すための仕組みが必要となります。

Work Rulesという本において、構造化された面接を行わない場合、面接官は最初の5分で相手を判断し、残りの時間はその判断を補強することに利用してしまうと言われています。 これは人事のプロでない私達では、より顕著に出てしまう傾向だと思われます。

こうした予測は、ある人物を本当に評価するというより、その人に関する自分の考えを確証するために面接するという状況を生み出す。 心理学者はこれを確証バイアスと呼ぶ。つまり「自分の信念や仮説を確証できるように、情報を探し、解釈し、優先順位をつける性向」だ。 ごくわずかなやりとりをもとに、私達はすでに持っているバイアスや信念に強く影響された判断を、即座に、無意識にくだす。 続いて、知らず識らずのうちに、受験者を評価することから自分の第一印象を確証する証拠を探すことに重心を移してしまう。

Work Rules 5章 直感を信じてはいけない P149 

また、面接方法とその後のパフォーマンスについて分析した研究によると、構造化面接は一般的な非構造的面接と比べて2倍程度の決定係数を持つことが示されています。 さらには構造化面接は、受験者にとっても満足度が高いと言われています。

1998年、フランク・シュミットとジョン・ハンターは、面接時の評価からパフォーマンスをどこまで予測できるかという85年にわたる研究をメタ分析し、 その結果を発表した。19の異なる評価方法を調べて分かったのは、よく行われいる非構造的面接の決定係数は0.14であり、社員の職務能力の14%しか説明できないことになる。(中略)

一般認識能力テストと並ぶのが構造的面接だ(26%)。受験者は、回答の質を評価する明確な基準を備えた一連の質問に答える。調査研究ではつねに構造的面接が利用されている。その基本的な考え方は、評価の変化は面接を受ける側の回答の良し悪しの結果であり、面接者の持つ基準が高いか低いか、発する質問が優しいか難しいかは関係ないというものだ。(中略)

構造的面接は受験者と面接者の双方にとって良い経験となるうえ、最も公正だと受け取られることも分かった

Work Rules 5章 直感を信じてはいけない P156

構造化面接で用意した質問により、相手のエンジニアとしての良さを、精度高く見つけられる仕組みを作り出すことができます。 そして、事前に作成して想定回答集により、相手の技術力の評価について、チームで一貫した判断基準を持つことができます。 相手の技術的な強みを正しく見つけ出せない場合は質問項目を見直し、正しく評価できな場合は判断基準の更新します。 これらの取り組みによって、面接の精度を改善することが可能となります。

面接の基本的な流れ

事前準備: 役割分担

  • 候補者の成果物は事前に見れるようにしておく
  • メインの面接者とサブの担当者は決めておく
  • メイン担当者は質問と相手の反応の確認に注意する
  • サブの担当者は議事録の作成に注力する

事前準備: 確証バイアスの洗い出し

自分がどのような先入観を相手に持っているのかを自覚することは、先入観を取り除くためにも重要です。 そのため、事前に提出された書類やGitHubのコードから、相手に対するポジティブ・ネガティブな先入観について書き出します。

そして、それら先入観を取り除くためにどうすれば良いのか、質問に対して相手がどのように答えれば、 その先入観は誤りだったと言えるのかを確認するためにチェックリストを作ります。

面接

ここで構造化面接を行います。 もし仕組みを整えることができれば、コードを書いてみてもらうのも良いでしょう。 この内容については別の機会に書きます。

最後に

2~3ヶ月前、面接方法を考えるために僕が情報収集したときのメモを公開してみました。 採用面接については奥が深すぎて、考えると本業がおろそかになってしまう可能性もあるため、 どこまで深ぼるべきなのか...というのが人の少ないベンチャー企業にて、採用に関わっているエンジニアの正直な気持ち。 このあたりはとてもむずかしい...。

本来ならば、入社後のパフォーマンスチェックもしたいです。

しかし、まったく異なる研究結果も出ている。職種によっては、訓練や経験が何の影響ももたらさないことが多いという。何カ月、ときには何年かけても、まったく向上しないのだ。たとえば心理療法士を対象にしたある調査では、免許を持つ「プロ」と研修生との間に治療成果の差は見られなかった。同様の研究結果は、大学入学審査員(入学希望者の勧誘・選考などを行う専門職)、企業の人事担当者、臨床心理士についても出ている (マシュー・サイド. 失敗の科学 失敗から学習する組織、学習できない組織 )

とあるように、面接に関してもフィードバックループが適切に回っていなければ、自分の判断が良かったのか。悪かったのかを判断することができません。 とはいえ、人の評価はとてもむずかしいです。成果に対するその人間の貢献度合いなんて、正しく計測することはできません。 そんな中でどうやってパフォーマンスチェックするのかというのが、最近の悩みです。

構造化面接についても、自分の知識が足りなすぎて相手の良さを引き出しきれてないケースを日々実感しているので、日々精進していかなければな〜と思う所存です。

参考資料

Azitに入社しました

TL;DR

  • 2019年7月にSpeeeを退職し、Azitに入社しました
  • Azitではインフラエンジニア・スクラムマスターをやってます
  • Speee、とても良い会社なので色んな人に勧めたい
  • Azit、とても良い会社だけど、死ぬほど人足りない

Azitに転職しました

2019年7月にSpeeeを退職し、Azitに転職しました。

転職の理由はプライベートの状況の変化で、自分の課題意識が変化したからです。 2018年くらいに父親が体を崩しまして、ちょいちょい見舞いとか病院の送迎とかをやっていました。 地元は公共の交通網が完全に死んでいてとても車なしで通院できる状況ではなかったので、 ぼくが2〜3時間掛けて実家に帰って、そこから車を1時間運転して病院に送迎していました。 ( ちなみに、今はもう父親は退院して元気にやってます。)

そんなことがありまして、田舎の交通事情は今すでに危機的だし、これからはもっとやばいことになるなーとか色々考えていたところ、 ちょうど地方の交通課題の解消に取り組んでいるAzitの人に会って話す機会がありまして、意気投合した結果、自分の技術力をこの課題の解消に使いたいと思ってAzitへ転職することにしました。

Azitでやってること

www.wantedly.com

Azitではだいたいここに書いてあることをやっています。 インフラをいちから作り直す仕事と、Railsコードの負債解消、将来マイクロサービス化を見据えた準備と、スクラムマスターっぽい諸々をやってます。

インフラエンジニアは僕1人しかいないので不安だらけですが、前前職同期の太田くんに技術顧問的に相談に乗って貰いながらなんとかやってます。

スクラムマスターぽい業務については、正しいものを正しくつくるカイゼン・ジャーニーユーザーストーリーマッピングの3つの書籍をベースに、様々な部署と協力しながらいいものが作れるように進めています。 この書籍をベースに僕たちのチームを作りたいぞ!って話をしたら、デザイナー、PM、データ分析、さらには広報、人事まで本を読んでくれたりしているので、 この機会をちゃんと掴んで良いチームにしていきたいな〜と思っとります。

Speee とても良い会社なので色んな人に勧めたい

まぁそんなこんなでAzitで楽しくチームづくりに関わっているわけですが、こういったふるまいの大部分は前職上司の大場さんに学びました。 面談等で諸々フィードバックを受けたとき、僕に知識がなくて言われたことを全然理解できなかったことが多々あったのですが、そういうときは理解するために必要な本を色々勧めて貰いました。 同じ本を読み、共通理解を持って物事を進めていくというやり方は、今の会社でもめちゃくちゃ使わせて貰っています。 正しい知識、HRTな態度、実行力の3つが揃えば、たいていの物事はちゃんと進んでいくことを大場さんの仕事を見ながら学ばせていただきました。

上長の荻原さんにも色んな面で助けられました。尊敬するエンジニアの一人です。 IDaaS関連システムについて、中々良い仕上がりになった ( 運用フェーズに関われてないので、本当に良かったのか後で教えてほしい...!! ) のは完全に荻原さんのおかげなので、感謝しかないです。今の現場でも、荻原さんならどう振る舞うかを考えながら開発しています。

tech.speee.jp

他にも pataiji と一緒にwebapp-revieeeをワクワクしながら一緒に作って、それをクリアコードの須藤さんにレビューして貰ったり、 井原さん、gfxさん、yhattさん、飯田さん、nisshieeさん、kohtaro24さんとかとか色んなタイプの優秀なエンジニアの人たちと一緒に働かせて貰いました。

Speeeには退職の意思を2018年末から伝えていました。 けれども、それ以降も今までと何も変わらずに、AWS re:inventに送り出して貰ったり、 いろんな業界の第一人者と呼ばれるエンジニアの方々にお会いさせて貰ったり、一緒に仕事する機会をもらいました。 そういう訳で、最後まで最高の会社でした。まじ感謝! そんな人たちと働きたいという人はこちらから応募して貰えるといいかと思います!

www.wantedly.com

Azitも人を募集しているよ!

Azitは若い人たちばかりで、エンジニアは30歳の僕が最年長です。 メンバーの学習意欲はとても高く、社内勉強会を開けば職種を問わずみんな来てくれて、 自分たちのサービスを改善するために勉強会で学んだ知識をどう活かすのかワイワイガヤガヤしながら楽しく開発しています。

そんなチームですが、エンジニアが本当に足りなくて困ってます! もし興味を持ってくれた人がいましたら、Wantedly、もしくは森岡個人にDMに連絡をください〜。 軽くお話だけでもぜひぜひ!

恒例のあれ

Amazon ほしいものリスト

AWS System Manager Sessions Manager のPort Forwardingを利用して踏み台を経由せずに手元からitamaeを実行する

AWS System Manager Session Managerとか、ちょっと冗談みたいな名前ですよね。 おいおいEKS化とかやってく!!

このドキュメントに書いてあること。

このドキュメントには、AWS System Manager Sessions Manager のPort Forwardingを利用して踏み台を経由せずに手元の端末からitamaeを実行するための設定方法 が記載されています。 具体的には、下記の3点になります。

  • 手元の端末にSession Manager Plugin をインストールする方法
  • インフラを構成するterraformのコードの一部(iamの設定部分)
  • itamaeを実行する手順

AWS System Manager Sessions Manager のPort Forwardingとは

aws.amazon.com

2019年8月28日に、AWS System Manager Sessions Manager の Port Forwarding という機能が発表されました。 これは、プライベートサブネットにデプロイされたEC2インスタンスと自分のPC間に、トンネルを作成してくれる機能です。 この機能を利用すると、踏み台サーバーを経由せずにEC2インスタンスにアクセスすることが可能になります。 今回はこの機能を利用して、踏み台を経由せずに、EC2インスタンスにitamaeを実行していきます。

手元端末に Session Manager Plugin をインストールする

https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html#install-plugin-macos

上記のドキュメントにaws cliに Session Manager Pluginをインストールする方法が記載されています。 実際実行するコマンドは下記のようになります。

$ curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/mac/sessionmanager-bundle.zip" -o "sessionmanager-bundle.zip"
$ unzip sessionmanager-bundle.zip
$ sudo ./sessionmanager-bundle/install -i /usr/local/sessionmanagerplugin -b /usr/local/bin/session-manager-plugin
$ session-manager-plugin

Terraform設定

EC2 instanceに対してSSM Port Forwardingができるように権限を付与する

AWS SSM Port Forwardingを利用して EC2 instanceにアクセスするためには、 EC2 instanceの IAM Roleに対して、AmazonSSMManagedInstanceCoreのポリシーを付与する必要があります。 僕は下記のTerraformのコードを利用して、ポリシーの付与を行いました。

resource "aws_iam_instance_profile" "default" {
  name = "${var.name}-profile"
  role = "${aws_iam_role.default.name}"
}

resource "aws_iam_role" "default" {
  name               = "${var.name}"
  path               = "/"
  assume_role_policy = file("${path.module}/assume_role_policy.json")
}

// SSMのポリシーを付与する
data "aws_iam_policy" "ssm_core" {
  arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_role_policy" "default" {
  name   = "${var.name}-policy"
  role   = "${aws_iam_role.default.id}"
  policy = "${data.aws_iam_policy.ssm_core.policy}"
}
// assume_role_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}

EC2 instanceにssm agentをインストールする

EC2インスタンスにおいてAWS SSMを利用するには System Manager Agentをインストールして、起動する必要があります。 今回はその設定をEC2インスタンスuser_data にて行うことにしました。 そのterraformの設定が下記のようになります。

resource "aws_instance" "ap-northeast-1a" {
  ami                         = "xxx"
  associate_public_ip_address = true
  availability_zone           = "ap-northeast-1a"
  instance_type               = "t3.medium"
  key_name                    = "xxx"
  monitoring                  = true
  ebs_optimized               = true
  disable_api_termination     = false
  source_dest_check           = true
  subnet_id                          = "$var.subnet_id"
  vpc_security_group_ids      = "${var.security_group_ids}"
  iam_instance_profile        = "${var.instance_profile_name}"

  tags = {
    Env  = "${var.env}"
    Name = "${var.name}"
  }

  user_data = file("${path.module}/install.sh")
}
#! /bin/bash

sudo yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
sudo systemctl enable amazon-ssm-agent
sudo systemctl start amazon-ssm-agent

itamaeの実行

上記でSystem Manager Port Forwardingを実行する環境が整ったので、あとはitamaeを実行するだけです。 itamaeの実行は .ssh/config にちょっとした設定を追加して、aws ssm start-session コマンドを実行すれば、後は itamae コマンドを実行するだけです。 ということで下記の手順で進めていきましょう。

.ssh/configの設定

Host xxx-staging-app-1
  HostName 127.0.0.1
  User ec2-user
  Port 9999

itamaeの実行

aws ssm start-sessionコマンドを実行して、手元の端末とEC2インスタンスの間にトンネルを作成しましょう。

$ aws ssm start-session --target i-xxx \
  --document-name AWS-StartPortForwardingSession \
  --parameters '{"portNumber":["22"],"localPortNumber":["9999"]}'

Starting session with SessionId: shuhei.morioka-xxx
Port 9999 opened for sessionId shuhei.morioka-xxx
Connection accepted for session shuhei.morioka-xxx

トンネルの作成が完了したら、あとはいつものようにitamaeのコマンドを実行していきます。

bundle exec itamae ssh -h xxx-staging-app-1 --node-yaml roles/xxx-staging-api/node.yml roles/xxx-staging-api/default.rb
 INFO : Starting Itamae... 
 INFO : Loading node data from /Users/xxx/node.yml...
 INFO : Recipe: /Users/xxx/default.rb
 INFO :   Recipe: /Users/xxx/itamae/cookbooks/git/default.rb
 INFO :     package[git] installed will change from 'false' to 'true'

ということで無事に、踏み台を経由せずに itamaeを実行することができました! 今の時代あんまり需要ないかもですが、この記事が誰かの手助けになるといいっすな。

SREについてDevOpsの違いや各種用語についてのまとめ

自分用メモです。

DevOpsとSREの違い

DevOpsとは開発(Development)と運用(Operations)を組み合わせた言葉であり、開発担当者と運用担当者が連携して協力し、さらには両担当者の境目も曖昧にする開発手法を指します。 厳密な定義は存在しておらず、抽象的な概念にとどまっています。 その目的から、DevOpsは組織(より具体的には任意のプロダクトに関わる全ての人達)の課題解決にフォーカスしています。 しかしながら、解決するための具体的な方法について定めるようなものではなく、大きな方針や文化を大切にしています。

SREとはGoogleのVPoEであるBen Taylor Slossが作成した言葉であり、Googleが実践しているシステム管理とサービス運用の方法論です。 事業責任者とService Level Objective(SLOs)を定め、それを守ることに責任を持ちます。 その他にもトイルに対する対処方法であったり、ポストモーテム、自動化などなど様々な内容に関して基準や、具体的な解決の手順を定めています。 DevOpsと親しい概念であり、class SREは interface DevOpsをimplementsしているとも言えます。

SREもDevOpsもサービスをより良くするという目的については一致しています。しかしながら、そのアプローチ方法が異なります。 DevOpsは組織的な課題解決にフォーカスすることによってその目的を達成しようとするのに対して、 SREは様々なオペレーションをソフトウェアで解決できる形に置き換え、ソフトウェアエンジニアリングのアプローチでもって課題に対処していくことでその目的を達成しようとします。

DevOpsは幅広い文化や哲学を指すのに対して、SREは厳密に定義された手法を持っていると言えます。

SLO/SLI/SLA/Error Budget

サービスを管理するためには、継続的に計測可能で具体的な指標が必要です。 その指標は、ユーザーにとって価値のあるサービスの振る舞いになります。 Googleではこのサービスレベルを管理するために、SLI(Service Level Indicators)、SLO (Service Level Objectives )、SLA (Service Level Agreements) の3つの概念を用いています。

SLO

SLOsとは、Service Level Objectivesの略称で、サービスの信頼性の目標のことを指します。 SLOを定める際は、ユーザーがそのサービスを利用する上で何が大切なのかを考え、それを数値化して指標とすることが大切です。 なお指標は平均を利用するのではなく、パーセンタイルを利用することが望ましいです。具体的には下記2つのような例があります。

  • eg1. gRPCのリクエストレイテンシの99%が100 ms以下であること
  • eg2. 可用率 99.9%

サービスには適切な信頼性というものがあります。過度な信頼性はコストとしてサービスに跳ね返ります。 また、明示的にSLOを定めなければ、ユーザーはサービスに対して過度な信頼を寄せることもあります。 障害の大部分はサービスの更新時において発生します。 なので、サービスの成長・拡大と信頼性のトレードオフから適正な値を定める必要があります。

サービスのSLOは最初から完璧に決められるものではありません。 最も大切なことは、計測することによって得たものでサービスとそのユーザーに対する理解を深め、よりよいSLOを模索し続けることです。 そのため、一度決めて終わりではなく、継続的に見直していく必要性があります。

SLO Document: https://landing.google.com/sre/workbook/chapters/slo-document/

SLI

SLIとはサービスの品質を決める計測可能な指標です。 サービスの品質を決める指標なので、ユーザーの視点に最も近い指標を利用することをおすすめします。 良いイベント数 / 全体のイベント数という形で定義することをおすすめします。 一般的によく使われる例は下記のようになります。

  • 成功したリクエストの数 / 全体のリクエスト数
  • 100ms以内に完了するgRPCのリクエスト数 / 全体のgRPCリクエスト数

こちらは、Google Workbookに記載される指標の一覧です。

このように定義すると、SLIは必ず 0 ~ 100 %の値を取ります。 SLIを一貫したスタイルで設定することによって、アラートツールを作るとき、エラーバジェットを計算するとき、レポートを作成するときに、同じようなINPUTを望むことができるようになります。 最初のSLIは、最小の工数でできる物を選びましょう。 調査に1週間かかってJSの搭載に数ヶ月かかるぐらいなら、すでにWebサーバのログがあるのであれば、ログを使いましょう。

Error Budget

Error Budgetの概念は、下記の数式で表されます。

SLIの計測値 - SLO = Error Budget

具体的な例を使って説明します。 例えば、とあるサービスのSLI を HTTP Responseを返したリクエスト数/ Load Balancerに到達したリクエスト数 としたとして、SLOを99%と置いたとします。 ここで SLIの計測値が 10000/10000 であった場合、100リクエスト分追加で失ってしまったとしても、SLOを満たしていると言えます。

この失うことのできる100リクエストをError Budget(予算)と言います。

エラーバジェットが残る限り、システムのリリースは継続が可能です。 逆に言えば、エラーバジェットが尽きそうになったら、リリースの頻度を下げる or ロールバックする必要があります。 エラーバジェットが多ければプロダクト開発者はリスクを取ることが出来ますし、エラーバジェットが少なければプロダクト開発者はリスクを下げる仕組みづくりに取り組むことになります。 例え外部要因によって障害が起きたとしても、エラーバジェットは消費されます。 プロダクトマネージャーは、イノベーションを取るならSLOを下げ、安定性を取るならSLOを上げることになります。

Error Budgetがなくなったときの取り決めをしなければ、SLOは何の実効性も持ちません。 そのためこの取り決めはとても重要です。全てのステークホルダーが合意するまで、SLI、SLOは調整する必要があります。

Error Budget: https://landing.google.com/sre/workbook/chapters/error-budget-policy/

SLOのTime Window

SLOsは様々時間幅で定義されます。代表的なものは Rolling windowと Calendar Windowの2つです。 ウィンドウを選択するとき、考慮するべき要素がいくつかあります。

Rolling Windows移動平均でSLOを導出します。そのためSLOはユーザー体験とより近しい指標になります。 Calendar Windowsは期間を決めてSLOを導出します。そのため、ビジネス上の戦略を立てやすくなります。

時間窓の期間によって、プロジェクトが取る戦略は変わります。 短い時間窓は迅速な意思決定を可能にし、長い時間窓は戦略的な意思決定を可能にします。 Googleでは4週間のローリングウィンドウを採用しています。

functions-framework を利用したGoogle Cloud Functionsにおいてpubsubのテストをする方法

このドキュメントに書いてあること

これまで Google Cloud Functionsをローカル環境でテストするときは、cloud-functions-emulator という公式で提供されているツールが一般的に利用されていました。しかしながらこのツールは現在archiveされており、作者が2019年5月16日に作成した issueによると functions-framework という新しいツールの利用を推奨しています。

このドキュメントでは、functions-framework を利用して Google Cloud FunctionsをLocalから実行する方法、特に公式では提供されていない eventsトリガーを利用してpubsubのメッセージを読み込ませる方法について記載します。

※ なお、2019年6月26日になっても公式ドキュメントでは @google-cloud/functions-emulatorを利用するようにと書かれています。 https://cloud.google.com/functions/docs/emulator?hl=ja#getting_started

スクリーンショット 2019-06-26 16.07.17.png

functions-frameworkとは?

Node.jsを利用してFaaSを書くためのOSSフレームワークです。このフレームワークを利用して書かれたコードは Cloud Functionsだけでなく、Cloud Runなどでも利用することができます。

そのようなことを目的としていると公式ドキュメントには書かれているものの、Cloud Run複数のAPIを持つ場合のケースに対応しておらず、現状では Cloud Functionsを Localで起動するためのツールとして使われることがメインになりそうです。Cloud Functionsでの利用であれば、Localの開発環境のみinstallするだけですぐに利用することができます。

npm i -D @google-cloud/functions-framework

このあたりの実装を読んでみると、内部でexpressサーバーを起動して、利用者が作成した functionをラッピングしていることが見て取れます。実行は簡単で、ラッピングしたいfunctionを下記のように指定してコマンドを実行すれば動きます。

npx functions-framework --target=helloWorld

実際に動かしてみる

プログラムの用意

僕が実際に利用しているプログラムからの抜粋です。cloud pubsubのmessageを受け取った後、その中身を見てSlackに通知しています。

export async function slack_reporter(data: any) {
  const dataBuffer = Buffer.from(data.data, "base64");
  const body = dataBuffer.toString("ascii");
  const client = await SlackClient.create();
  await client.post(body);
}
import { WebAPICallResult, WebClient } from "@slack/client";

export class SlackClient {
  public static async create(): Promise<SlackClient> {
    if (!this.instance) {
      const token = process.env.SLACK_TOKEN as string;
      const channel = process.env.SLACK_CHANNEL_ID as string;
      this.instance = new SlackClient(token, channel);
    }

    return this.instance;
  }

  private static instance: SlackClient;

  private slackCleint: WebClient;
  private channel: string;

  constructor(token: string, channel: string) {
    this.slackCleint = new WebClient(token);
    this.channel = channel;
  }

  public async post(text: string): Promise<WebAPICallResult> {
    return this.slackCleint.chat.postMessage({
      username: "ERP-HR Bot",
      channel: this.channel,
      text,
    });
  }
}

pubsubのmessageを作成する

pubsubで送信されるmessageのフォーマットについては、公式ドキュメントによると下記のように指定されています。

{
  "data": string,
  "attributes": {
    string: string,
    ...
  },
  "messageId": string,
  "publishTime": string
}

このように色々なパラメータが存在していますが、今回のケースで利用するのは data のみです。理由については公式ドキュメントに記載されています。ということで messageを作成していきます。この dataパラメータはbase64エンコードする必要があるため、下記のコマンドでエンコードします。

$ echo -n "hogehoge" | base64
aG9nZWhvZ2U=

これをmessageで送信するjsonに組み込むと下記のようになります。

{
  "data": "aG9nZWhvZ2U="
}

Local環境でCloud Functionsを起動する

公式のドキュメントによると、functions frameworkを起動する際のoptionは --port, --target, --signature-typeの3点です。ここでは実行したい functionは slack_reporterであり、トリガーはpubsubにしたいので、下記のように設定をしました。

$ npx functions-framework --target=slack_reporter --signature-type=event

Serving function...
Function: slack_reporter
URL: http://localhost:8080/

呼び出し

Cloud FunctionもLocalで起動したので、次は起動しているCloud Functionを実行していきます。ここで Functions Frameworkのissueを読んでいくと次のようなissueが見つかります。

https://github.com/GoogleCloudPlatform/functions-framework-nodejs/issues/37

そしてこちらのPR上で、どのようにmessageを送信することが適切なのか議論されています。ということで、これから記述する方法は将来正しくない方法になってしまう可能性がありますが、とはいえ今試す必要がある人がいるとも思うので書いておきます。

curlを使って下記のように実行すると、Localで動いているCloud Functionsが pubsubのメッセージを読み込むことができます。

$ curl -X POST -H 'Content-Type:application/json; charset=utf-8' \
  -H 'ce-type: xxx' \
  -H 'ce-specversion: xxx' \
  -H 'ce-source: xxx' \
  -H 'ce-id: xxx' \
  -d "$(cat mock_pubsub.json)" http://localhost:8080

すると、こんな感じでslackに通知されました。めでたしめでたし。

スクリーンショット 2019-06-26 18.47.51.png

この理由については、このあたりのコードに書いてあるのですが、もうちょっと追加調査したいことがあるので、またの機会に書こうと思います〜。

SAM Localを利用してLocalで動かしているAWS Lambda からdynamodb-localにアクセスする方法

この記事に書かれていること

  • SAM CLIの環境構築方法
  • SAM CLIを使ってLocalでLambdaを起動する方法
  • SAM CLIを使ってLocalで起動しているLambdaから、Localで用意したDynamoDB containerにアクセスする方法
  • これらの処理を僕が趣味で作っているAWS Lambdaを例に説明します。

この記事に書かれていないこと

  • SAM CLIとは何か?
  • Lambdaを利用する際のwebpackの設定

利用環境

  • nodejs8.10
  • TypeScript 3.4.5
  • SAM CLI 0.15.0
  • python 3.7.2

事前準備

aws-sam-cliのinstall

Installing the AWS SAM CLI on macOS というAWS公式の手順に則ってinstallします。

aws-sam-cliは、pythonのバージョン 2.7、3.6、3.7 に対応しています。もし手元の環境がそれらのバージョンに一致していないのであれば、対応しているバージョンのpythonをinstallしましょう。なお2.7は2020年の1月にはメンテナンスが終了されますので、今から入れるのであれば 3以上にすると良いでしょう。

$ brew install pyenv
$ brew install pyenv-virtualenv
$ pyenv install 3.7.2
$ pyenv local 3.7.2
$ brew tap aws/tap
$ brew install aws-sam-cli
$ sam --version
SAM CLI, version 0.15.0

dynamodb-localのdocker imageをpull

こちらもamazon公式のdocker imageを利用します。下記のコマンドを実行してdocker imageをpullしましょう

docker pull amazon/dynamodb-local

SAM Localテスト用データ作成

aws-sam-cliを使ってLocalからLambdaを起動するためのデータを作成します。今回は、シンプルにAPI Gatewayから起動することにします。

sam local generate-event \
  apigateway aws-proxy \
  --path datadog_report \
  --method GET > events/event_apigateway.json

このコマンドによって作成されたjsonこちらになります。

実装

docker-composeの設定

# docker-compose.yml
version: "3"

services:
  dynamodb-local:
    container_name: dynamodb
    image: amazon/dynamodb-local
    build: ./
    ports:
      - 8000:8000
    command: -jar DynamoDBLocal.jar -dbPath /data -sharedDb
    volumes:
      - ./data:/data
    networks:
      - lambda-local
networks:
  lambda-local:
    external: true

この設定において重要な点は3点あります。

1点目は、DynamoDB localのコマンドオプションに -dbPath /data を指定している点です。-dbPathでdockerがマウントしているvolumeに書き出すことによって、指定したディレクトリにデータを吐き出させるようにしています。こうすることで、データを永続化しています。-inMemoryオプションを使ってしまうと、毎回データが削除されてしまうので、開発時にそのオプションを利用するのは少し手間が掛かってしまうでしょう。(テストのときはあると良さそうです)

2点目は、DynamoDB localのコマンドオプションに、 -sharedDbオプションを指定しているところです。-sharedDbオプションを指定しない場合、データはmyaccesskeyid_region.db というフォーマットで格納されます。これはこれで、毎回起動するときにそのあたりのパラメータをちゃんと設定できていればよいのですが、今回は簡単のため-sharedDbオプションを指定しています。

3点目は、networksを指定しているところです。aws-sam-localによってlocalで実行されるLambdaは、起動時にdockerのnetworkを指定することができます。ここで指定したnetworksを aws-sam-localの起動時にも利用することによって、localで起動しているLambdaから、このdocker containerにアクセスすることができるようになります。

これらDynamoDB localのオプション内容については、公式ドキュメントに記載があるので参照してください。ということで設定ができたので、下記コマンドを実行してDynamoDB Localの環境を構築しましょう。

docker network create lambda-local
docker-compose up

typescript

ぼくが趣味で作っている、AWS Lambdaのコードから取ってきたやつです。 https://github.com/selmertsx/datadog_slack_report

今思えばちょっと設計に改善の余地ありですな...。この後新しい機能を追加予定なので、そのときにでもリファクタリングしようと思います。一旦必要そうなもののみ引っ張ってきました。

// https://github.com/selmertsx/datadog_slack_report/blob/c4e59fdb60b2e190bd58f7e823268d8b697e3dfb/src/index.ts
import { APIGatewayEvent, Callback, Context } from "aws-lambda";
import moment from "moment-timezone";
import "source-map-support/register";
import { Billing } from "./Billing";
import { SlackClient } from "./SlackClient";

export async function datadog_handler(event: APIGatewayEvent, context: Context, callback: Callback) {
  const fromTime = moment({ hour: 0, minute: 0, second: 0 })
    .tz("Asia/Tokyo")
    .subtract(1, "days")
    .format("X");

  const toTime = moment({ hour: 23, minute: 59, second: 59 })
    .tz("Asia/Tokyo")
    .subtract(1, "days")
    .format("X");

  try {
    const billing = new Billing();
    const report = await billing.calculate(fromTime, toTime);
    const slackClient = new SlackClient();
    await slackClient.post(report.slackMessageDetail());

    callback(null, {
      statusCode: 200,
      headers: {
        "Content-Type": "application/json;charset=UTF-8",
      },
      body: JSON.stringify({ status: 200, message: "OK" }),
    });
  } catch (err) {
    throw new Error(err);
  }
}
// https://github.com/selmertsx/datadog_slack_report/blob/e078d2427806f3f9b402a3af1fbe79c98b0e2a5a/src/DynamoDBClient.ts
import { DynamoDB } from "aws-sdk";
import { ReservedPlan } from "./typings/datadog";

export class DynamoDBClient {
  private client = new DynamoDB.DocumentClient({
    endpoint: "http://dynamodb:8000", // ここが重要!!!!!
    region: "ap-north-east1",
  });

  public getReservedPlans(): Promise<ReservedPlan[]> {
    return new Promise<any>((resolve: any, rejects: any) => {
      this.client.scan({ TableName: "DatadogPlan" }, (error, data) => {
        if (error) {
          rejects(error);
        } else if (data.Items == undefined) {
          resolve([]);
        } else {
          const results: ReservedPlan[] = [];
          data.Items.forEach(item => {
            results.push({ productName: item.Product, plannedHostCount: item.PlannedHostCount });
          });
          resolve(results);
        }
      });
    });
  }
}

さて、長々とコードが書いてあるのであれなのですが、重要なのは1点だけです。DynamoDBのendpointについて http://${dynamodb-localのcontainer名}:8000 としていることです。これによってSAM Localで起動したAWS Lambdaから、LocalのDynamoDBにアクセスすることができます。

  private client = new DynamoDB.DocumentClient({
    endpoint: "http://dynamodb:8000", // ここが重要!!!!!
    region: "ap-north-east1",
  });

起動方法

ということで、ここまでやったら後は起動するだけ。起動する際は、 sam local invoke コマンドの --docker-network オプションに、先程 docker-compose.yml で指定した network名を設定してみましょう。具体的には下記のコマンドになります。

$ npx webpack --config webpack.prod.js
$ sam local invoke --docker-network lambda-local -e events/event_apigateway.json --env-vars .env.json DatadogReport

2019-04-25 10:30:30 Found credentials in environment variables.
2019-04-25 10:30:30 Invoking index.datadog_handler (nodejs8.10)

Fetching lambci/lambda:nodejs8.10 Docker container image......
2019-04-25 10:30:33 Mounting /Users/shuhei.morioka/project/speee/datadog_slack_report as /var/task:ro,delegated inside runtime container
START RequestId: dbefc77e-42dc-1d21-a444-0abc44875df5 Version: $LATEST
END RequestId: dbefc77e-42dc-1d21-a444-0abc44875df5
REPORT RequestId: dbefc77e-42dc-1d21-a444-0abc44875df5  Duration: 4299.35 ms    Billed Duration: 4300 ms        Memory Size: 256 MB     Max Memory Used: 121 MB

ということで、Localで動いているAWS LambdaからDynamoDB Localにアクセスすることができました〜。