サーバーレスで作るセキュリティアラート自動対応フレームワーク

技術部セキュリティグループの水谷 ( @m_mizutani ) です。ここしばらくはフルリモートワーク体制になったので運動不足解消のためウォーキングをしたり筋トレしていたら、リモートワーク前より健康になった疑惑があります。

クックパッドのセキュリティチームでは日々のセキュリティ監視を効率化するため、独自のフレームワークを構築して利用しています。具体的には、セキュリティアラートが発生した際に自動的に様々なデータソースから関連情報を収集し、収集した情報をもとにアラートのリスクを評価、そして評価結果をもとに自動対応をするという一連のワークフローを実現するフレームワーク DeepAlert をAWS上にサーバーレスで構築しました。この記事では、このフレームワークを構築した経緯やアーキテクチャ、仕組みについて解説します。

セキュリティアラートの対応

ここでは、セキュリティ侵害が発生している可能性があるものについて管理者に対応を求めるようなメッセージをセキュリティアラートと呼んでいます。これはセキュリティ防御・監視装置(Firewall、IDS/IPS、WAF、AV、EDR、などなど)から直接アラートとして発せられることもありますし、ログの中から見つかった不審な活動や外部からの連絡によって発覚する事象など、発生の経緯は様々です。共通しているのは組織内でセキュリティ上の問題が発生している可能性があり、状況に応じてなんらかの対応が必要である、ということです。今回は特にセキュリティ防御・監視装置から発報されるものを中心に説明します。

セキュリティアラートは組織内のセキュリティの侵害の可能性を見つけてくれる便利な情報ではありますが、実際に脅威ではないものを発報してしまうケースが多々あります。これは主に次の2つの理由が挙げられます。

  • 防御・監視システムは見える範囲に限りがあり、別システム(特に社内システムなど)の情報とつきあわせないと判断できないこともセキュリティアラートとして発報してしまう
  • 防御・監視システムは一般化されているため、それぞれの会社や事業部ごとに特有の業務や文脈に対応しきれない場合がある

これによっていわゆる誤検知・過検知が発生してしまうため、担当するセキュリティエンジニアが都度調査・分析してアラートの実際の深刻度を判断する必要があります。この作業は組織のセキュリティを維持するための大切な業務ですが、日々持続的に発生するために他の業務を徐々に圧迫してしまい、件数が多くなることで担当者を疲弊させてしまいます。

通常、防御・監視システムにはホワイトリスト機能が備わっており、指定した条件に一致するアラートは発報しないよう設定ができます。しかし、実際には先述した通り他のシステムの情報と突き合わせないと判断が難しいケースやホワイトリスト機能では除外条件を表現しきれないケースがあり、容易に対応できるものでもありません。

まとめると、実際のセキュリティアラートの対応では、まずアラートの調査と分析(影響有無の判断)があり、その後で必要に応じて何らかのアクションをする、といった流れになります。この一連の作業を効率化するために、自律的に複数の情報を組み合わせて必要な対応をしてくれる仕組みを作ることにしました。

設計方針

セキュリティの対応をコード化する

この仕組を作るにあたっては対応の部分をなるべくコード化する、ということを指針として取り組みました。近年、サービス開発の分野ではインフラの管理にDevOps や Infrastructure as Code の概念によってリソースの管理や手続きをコード化する、という取り組みが盛んになっていると思います。これはセキュリティにも応用できる考え方であり、コード化することによって対応の自動化だけでなく、対応内容の明確化や変更履歴の管理、手続きに対してテストができるようになる、といった恩恵を受けることができます。

セキュリティアラートの対応はその組織の構成、活動内容、文化や発生時の文脈に依存するも多くあり、全てのアラートを一律に自動化して対応できるものではありません。しかし、多くのアラートについては決められた手順で関連する情報を調査し、得られた情報をもとに定められた基準に従ってリスクを評価することができると考えられます。自分が前職でSOC(Security Operation Center)に勤めていたときも、明文化こそされていなかったものの定型化された対応は多く存在し、かつアナリストの間で共有されていました*1。もちろん、定型的に判断しきれないアラートや新しい脅威に対してはセキュリティエンジニアによるきめ細やかな分析が必要になりますが、そうでないものはなるべく自動化することで、エンジニアがより本質的な作業に注力できるようになります。

そのため、このフレームワークを作るにあたっては「誰がやっても同じ結果になるものは人手を介さないようにする」という機能の実現を目指し、全体をコード化するという方針で設計しました。

その他の機能要件

セキュリティのコード化以外にも、次のような要件を考慮して設計しました。

  • 容易な機能拡張:関連する情報の検索や最終的な対応は、様々なデータソースやインターフェイスに対応する必要があります。また、状況に応じて機能を追加・削除していくと考えられるため、なるべく自動対応のメインのシステムとは疎結合になるようにするべきと考えました。
  • 低コスト運用:「横断的に複数のデータソースを使ってアラートの精度を上げる」といったアプローチは新しいものではなく、昔からSIEM(Security Information & Event Manager)でも同じような取り組みがされていました。しかし、多くのSIEMはリアルタイムにイベントを処理するような設計となっているため高い処理能力が求められ、高価になってしまう傾向があります。もちろんお金で解決すべきところにはお金を投入するべきですが、セキュリティは直接的にビジネスに貢献するものではないこともあり、工夫次第でコストを抑えられるならそうするべきと考えました。
  • 弾力性:現状、クックパッドでは平均して一日あたり数件のアラートしか発生していませんが、今後の新しい脅威や方針の変化にともなってアラートの流量が増える可能性があります。そうした場合にスケールアップでしか処理量の性能をあげられないとするとすぐに限界がきてしまい、対応が滞ってしまう可能性があります。もともとの設計で速やかにスケールアウト・スケールインができるような弾力性を備えておくことで、突発的な流量の変化にも耐えられるようになります。

セキュリティアラート自動対応フレームワークの実装

アーキテクチャ概要

f:id:mztnex:20200317083451p:plain

設計で説明したような機能を実現するため、 DeepAlertというAWS上にサーバーレスで構築されたフレームワークを実現しました。これはセキュリティアラートを外部から受け取り、Inspector、Reviewer、Emitterという3つの役割を持つAWS Lambda Functionと連携して動作します。それぞれのLambda Functionの役割は次のとおりです。

  • Inspector:アラートに出現したIPアドレス、ドメイン名、ユーザ名に関して内外のデータソースにアクセスし、必要な情報を収集します。例えば外部から接続してきたIPアドレスであればブラックリストに掲載されているか、ドメイン名であればどういったサービスに使われているか、内部システムのユーザ名であればアラート直前までの行動ログなどを収集し、それらの結果をDeepAlertに返します。
  • Reviewer:アラートおよびInspectorが調査した結果を元にそのアラートのリスクを評価します。評価結果はシンプルに safe(影響なし)、unclassified(不明)、urgent(影響あり、要対応)の3種類のみにしています。
  • Emitter:Inspectorの調査結果、そしてReviewerの評価結果を元に対応を請け負います。対応も色々種類があり、調査や評価の結果をSlackなどを通じて通知する、外部の特定のIPアドレスからの接続を遮断する、あるいは対象ホストを隔離する、というような処理を想定しています。この対応も、影響ありの状況だったら即座に遮断したり、影響がなければ記録だけして通知はしない、というような評価結果に基づいた動作の振り分けも考慮しています。

これらのLambda FunctionはDeepAlertとは独立しており、特にInspectorとEmitterは任意の種類、数を接続することが可能になっています。それぞれAWSのSNS(Simple Notification Service)、SQS(Simple Queue Service)、Step Functionsを使うことでDeepAlertと連携しています。より具体的なアーキテクチャが次の図になります。大まかな動作として3段階に分かれており、Inspectorを動かす 1) 調査フェイズ、Reviewerを動かす 2) 評価フェイズ、そしてEmitterを動かす 3) 対応フェイズとなっています。

f:id:mztnex:20200317083605p:plain

この通り、DeepAlertはInspector、Reviewer、Emitterを動かすためのフレームワークとして実装しました。これまで様々な改良を続けてきたのでやや異なる部分はありますが、この仕組で約2年ほど運用し、その間にInspector、Reviewer、Emitterを必要に応じて入れ替えてきました。

Lambda、Step Functions、SNS、SQS、DynamoDBのみでサーバーレス構成として実装したため、料金は完全に利用量に基づいて計算されるようになりました。具体的には後述しますが、流量が少なければ非常に安価に使うことができます。また、各サービスにリソースの上限は設けられているものの、その限界までは人間の手を介することなく自動的にスケールアウト・スケールインしてくれます。これによって運用における金銭的コスト・人的コストの両方を極力抑えられています。

Pluggableな機能拡張

要件のパートで説明したとおり、情報収集をするInspectorと最終的に対応をするEmitterは状況の変化に応じて機能を追加・変更・削除していく頻度が多くなっています。我々はこの仕組を2年ほど運用していますが、その過程でも監視すべき対象が変わったりチームの運用方法にあわせて調査対象や対応方法が変化しています。

そこで、Inspector、EmitterはDeepAlert本体とは疎結合な形でデプロイできるようにしました。DeepAlertのリソースはAWS SAM(Serverless Application Model)およびCloudFormationでまとめてデプロイしていますが、InspectorやEmitterはそれぞれ任意の複数種類のLambdaをDeepAlertとは別のSAMでデプロイしてもいいですし、Cloud9で実装したものをデプロイするでも問題ありません。InspectorとEmitterはそれぞれSNS経由で必要な情報を受け取って起動し、InspectorはSQS(ContentQueue)で調査結果をDeepAlert側に戻します。また、Inspectorが関連する情報を調査する過程で、新たに調査すべき要素(例えば調査対象のユーザが使っていた別のIPアドレスや、マルウェアのハッシュ値からそのマルウェアが使っていたCommand & ControlサーバのIPアドレスなど)が発見された場合も、その情報をSQS(AttributeQueue)を通じてDeepAlertに戻して再度その要素についてInspectorが調査する、というフィードバックの仕組みも実装されています。このようにSNSとSQSだけを用いてintegrationする仕組みにすることで、Inspector、Emitterの動作がDeepAlert全体の動作に影響を与えないようにしています。

ちなみに、これまでクックパッド内では次のようなInspector、Emitterを運用してきました。カッコ内はアクセスするデータストアやサービスになります。(すでに利用しなくなったものも含みます)

  • Inspector
    • IPアドレス、ドメイン名、ファイルのハッシュ値がマルウェアに関連しているかを調査する(VirusTotal、Malwarebytes)
    • 出現したURLのスキャンし、どのようなサイトだったのかの情報を調査する(urlscan.io)
    • 社員の誰がそのIPアドレスを利用していたのかというログの抽出(社内のIPアドレス管理DB)
    • そのホストに自社管理のセキュリティソフトがインストールされているかの確認(CrowdStrike Falcon)
    • IPアドレス、ドメイン名、ユーザ名に関連する直近のログの抽出(社内のセキュリティログ検索基盤)
  • Emitter
    • 評価結果に基づいてアラートの通知(Slack)
    • アラート対応の割り振り(PagerDuty)
    • 調査結果アラート情報をまとめて保存(GitHub Enterprise)

Emitterについては、本来は被害をうけたと見込まれるホストをネットワークから隔離したり、証拠保全のプログラムを実行したり、ということも想定はしていました。しかし、幸いにも私自身が入社して以来、そういったことを即座に実行する必要があるようなインシデントに遭遇したことがなく、サービスに影響するような能動的な対応をどのくらいの確信度で実行するべきかというルール化ができていないため、そういった機能はまだ実装していません。これについては今後の課題としたいと考えています。

一般的なプログラミング言語でコード化したポリシー

先述したとおり、Reviewerは調査で集められたアラートの情報をもとに、そのアラートが実際の被害を及ぼしたのかを評価します。評価の方法や仕組みは Lambda Function にコードとして自由に記述できるようにしました。現在、クックパッド内ではGo言語を使ってポリシーを記述していますが、DeepAlert側で規定した Lambda Function に対する入力と出力のインターフェースに則っていればどのような言語で記述できます。

SIEMをはじめとする多くの製品では独自の記法でポリシーを記述ようになっています。これはポリシーに記述する要素を厳選し、入力する内容を減らすことで容易に表現できることを目的としていると考えられます。このような仕組みになっていることで、単純なポリシーであれば低い学習コストで記述できるようになります。しかし複雑な条件を扱う必要が出てくると、ポリシーを分割して見通しを良くするということができなかったり、任意のテストができないために検証のコストが大きくなってしまう、という課題に直面しがちです。また、デバッグの手段が用意されていない場合も多く、ひたすらトライ&エラーを繰り返して検証する必要がある、という問題にも悩まされます。ポリシーの記述力もあまり柔軟ではない場合が多く、愚直な処理を繰り返し書かなければいけなかったり、ちょっとしたデータ形式の変換などができず消耗するといったこともしばしばありました。

DeepAlertを実装する際、一般的なプログラミング言語でポリシーをコード化することでこれらの問題の多くを解決できると考えて、入出力のインターフェースだけを定義しました。DSLやライブラリを使って評価するような機能を提供しないことによって、言語の種類に対する依存も極力ないようにしました。これによって、通常のプログラミングにおけるコード整理やテストの技法を取り込むことが可能となり、ポリシーが複雑化しても見通しがよくテスト可能な形で記述することができるようになります*2

Reviewerの入出力定義

Reviewerに対する入力のサンプルを以下に示します。

{
    "id": "61a97323-b7dc-4b13-a30d-7b423388da5f",
    "alerts": [
        {
            "detector": "AWS GuardDuty",
            "rule_name": "High Severity Finding",
            "rule_id": "guardduty/high_sev",
            "alert_key": "xxxxxxxxxxxx",
            "description": "Unusual resource permission reconnaissance activity by PowerUser.",
            "timestamp": "2020-03-12T18:07:10Z",
            "attributes": [
                {
                    "type": "ipaddr",
                    "key": "remote IP address (client)",
                    "value": "198.51.100.1",
                    "context": [
                        "remote",
                        "client"
                    ]
                },
                {
                    "type": "username",
                    "key": "AWS username",
                    "value": "mizutani@cookpad.com",
                    "context": [
                        "subject"
                    ]
                }
            ]
        }
    ],
    "sections": [
        {
            "author": "addrmap",
            "type": "host",
            "content": {
                "activities": [
                    {
                        "last_seen": "2020-03-12T00:46:17.000928Z",
                        "principal": "mizutani",
                        "remote_addr": "198.51.100.1",
                        "service_name": "AzureAD",
                        "owner": "Cookpad"
                    },
                    {
                        "last_seen": "2020-03-12T12:39:11Z",
                        "principal": "mizutani",
                        "remote_addr": "198.51.100.1",
                        "service_name": "Falcon",
                        "owner": "Cookpad"
                    }
                ]
            }
        }
    ]
}

元にしているのはAmazon GuardDutyから発報されたアラートです。説明のためにいろいろと省略していますが、基本となる要素は含まれています。(詳細な定義については こちら から参照することができます)

まず alerts がセキュリティ監視・防御システムから発報されたアラートになります。このアラートについても独自のフォーマットになっているため、発報するシステムとDeepAlertの間で1つLambdaを挟んでフォーマットの変換をしています。 attributes の部分にはそのアラートに出現した属性値になります。それぞれIPアドレスやユーザ名といった型を付けているのは従来のSIEMなどと同じですが、 context というフィールドをもたせることでその属性値の意味がわかるようにしています。これは、例えば「Source IP address」という型でアラートの属性値が正規化されていたとしても、それが内部と外部のどちらのネットワークを意味するのかであったり、何か攻撃をした側なのか、それとも攻撃を受けた側なのかということが発報時の文脈によって変わってしまうという問題に対応するための説明用フィールドとなっています。

そして、Inspector によって収集された情報を格納したのが sections になります。ここでは社用PCがどのIPアドレスからどのサービスを使っているかというaddrmapという内製ツールからの情報が付与されています。これを見ることで AzureAD および CrowdStrike Falcon でもアラートがあがったIPアドレスから同様に接続があったことが示されています。こういった情報をReviewerが参照し、ポリシーで影響あり・なしの判断ができるのであればそれを出力として伝える、判断できないのであればセキュリティエンジニアの判断に委ねる、といった処理をしています。

この入力のスキーマについてはSTIXのような既存の脅威情報を記述する構造を利用することも考えましたが、本来の目的が違うこと、我々がやりたいことから見て機能が過剰であること、互換性を維持する意味があまりないことから独自の形式にしました*3

一方、出力についてはシンプルで、severityreason の2つを入れるのみです。先述したとおり、severitysafeunclassifiedurgent の3段階でのみ表現されます。

{
    "severity": "safe",
    "reason": "The device accessing to G Suite is owned by Cookpad"
}

記述されたポリシーの例

具体的なポリシーの記述例を以下に示します。

// AWSへの不審なログインのアラートを評価するポリシーの例
func handleAlert(ctx context.Context, report deepalert.Report) (deepalert.ReportResult, error) {
    for _, alert := range report.Alerts {
        // アラートが対象のものでなかったら評価しない
        if report.RuleID != "guardduty/high_sev" {
            return nil, nil
        }

        // オフィスのIPアドレスからのアクセスの場合はこのポリシーでは評価しない
        if hasOfficeIPAddress(alert.Attributes) {
            return nil, nil
        }
    }

    // Inspectorによる調査結果を抽出
    reportMap, err := report.ExtractContents()
    if err != nil {
        return nil, err
    }

    // アクセス元のホストに関して Inspector が取得した情報をチェック
    for _, hostReports := range reportMap.Hosts {
        for _, host := range hostReports {
            for _, owner := range host.Owner {
                // そのホストの所有者が Cookpad のものであると確認できるログがあった場合、
                if owner == "Cookpad" {
                    return &deepalert.ReportResult{
                        // Safe(影響なし)と判断する
                        Severity: deepalert.SevSafe,
                        Reason:   "The device accessing to G Suite is owned by Cookpad.",
                    }, nil
                }
            }
        }
    }

    return nil, nil
}

func main() {
    lambda.Start(handleAlert)
}

このコードは説明のために簡略化していますが、おおまかな流れは実際のものと変わりません。先程の入力データの例では、不審なログインのアラートに対して、Inspectorが別の社内向けサービスを利用していたという情報を付与していました。このポリシーではその付与された情報を確認して、それが社員のPCが使っているIPアドレスからのアクセスなのか、それとも全く関係ない海外のサーバなどからのものなのかを確認し、もし社員のPCであると考えられる場合は safe(影響なし)という判断を返します。もし判断に足る情報がなければ、何も返さないことで unclassified(不明)と判定されます。

このポリシーとして記述された関数 handleAlert を用いることによって、アラート評価のテストを記述することができるようになります。この例では1つアラートに対するポリシーだけを記述していますが、実際には複数種類のアラートに対応できるようなコードを書く必要があります。新しくポリシーを追加したり、既存のものを変更した時、意図していない変更がまぎれていないかを確認するために常にテストで確認ができることで、自信を持ってポリシーをデプロイすることができるようになります。

その他のアーキテクチャの工夫

  • DynamoDBのベストプラクティスにもあるように、データストアは一つのDynamoDBのテーブルに押し込めています。このテーブルは複数アラートの集約、アラート情報の一時的な保持、新しく出現した属性値の管理、Inspectorの調査結果の保持などに利用しています。
  • 調査フェイズと評価フェイズにおいてLambdaの実行制御にStep Functionsを挟んでいるのは、アラートの到着から調査を開始するまでにわざと遅延を入れるためです
    • Inspectorの調査活動でも特にログを検査するタイプのものは、アラートが到着した直後ではまだログを参照できる状態になっていないことがあるため、数分程度待ってからInspectorを起動します
    • また、Inspectorが疎結合で任意の数実行されることから、DeepAlert側では同期的にInspectorの制御はしていません。そのため非同期に実行されたInspectorの結果を待つためにもStep Functionsを使っています

DeepAlert導入の効果

深刻度の自動評価による運用負荷の軽減

この仕組みを運用し始めておよそ2年ほどになりますが、2019年の実績では約50%ほどのアラートを人間が確認する前に影響なしであることを確認できました。これによって、対応に割く時間を大幅に減らす事ができました。

実際には影響がなかった場合でも全体の傾向の変化があった場合には気づきたいので、Slackで「影響なしのアラートが発生した」ということだけは通知させています。

f:id:mztnex:20200317083704p:plain

また、「影響なし」以外の判断がされたものについても、GitHub Enterpriseでアラートの詳細を記載したIssueを作成し、その上で対応の管理をしています。Issueについても最初にセキュリティアラートとしてDeepAlertが受信した情報だけでなく、Inspectorが取得した情報もあわせて記載しています。例として以下に示しているIssueでは、セキュリティログ検索システムへのリンクやVirusTotalへInspectorが問い合わせて取得した情報もあわせて掲載しています。これらの情報だけでは機械的に判定ができなかったわけではありますが、セキュリティエンジニアが自分で調査する際にも同じような情報をもとに作業することになるので、これらの情報が予め掲載されているということは作業時間の短縮に繋がります。このような点から自動評価できなかったアラートに対しても運用の負荷が下がっていると言えます。

f:id:mztnex:20200317083720p:plain

〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜 中略 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

f:id:mztnex:20200317083735p:plain

コスト

結論から言うと、直近半年のデータから計算された実費用は一ヶ月あたりで約 $1.3 でした。アーキテクチャの説明でも述べた通り、DeepAlertはクラウドの金銭的運用コストおよび弾力性を考えてLambda、Step Functions、DynamoDB、SQS、SNSといったリソースのみで構成されています。原則としてどれも使ったリソース量に応じてのみ課金される構成が可能であり、対処するアラートの流量が少なかったり、アラートの発生頻度にむらがあるような場合でもコストを小さく抑えやすくなっています。

まとめ

DeepAlertは、既存のSIEMのように「複数のイベントを組み合わせて(精度の高い)セキュリティアラートを発報する」という考え方ではなく、「(精度の低い)セキュリティアラートに対して情報を付与し、精度を高める」といった戦略になっています。そのため、純粋な機能面でのできること自体はSIEMのサブセットという位置づけになってしまいますが、運用のしやすさやコストメリットなどから、このアプローチが最も効果的であると判断して取り組んできました。このように既存の製品だけにとらわれず、それが本当に必要であれば自分たちで作る、といったところまでやりきれるのが、事業会社でセキュリティをやる楽しさの一つなのではないかなと思います。

クックパッドではこうしたセキュリティにまつわる課題を一緒に解決していくエンジニアを絶賛募集しています。興味のある方はぜひこちらをご参照いただくか、ご質問などあれば水谷( @m_mizutani )などまでお声がけください。

*1:これは自分たちの業務を楽にするというだけでなく、SOC全体のクオリティを一定に保つ、という効果もあったと考えています

*2:かならずしも見通しがよいコードが書かれることが保証されるものではありませんが、できる余地があることが重要だと考えています

*3:ただしSTIXなどからDeepAlertの形式に変換するというのは意味があるかもしれないので、必要があれば実装したいと考えています

テストケース作成を仕様詳細化の手段とする実験

こんにちは。 テストエンジニアからサービス開発エンジニアにロールチェンジした、茂呂一子です。 先日リリースしました、iOSクックパッドアプリのリニューアルプロジェクトに参加し、サービス開発エンジニアとしての第一歩を踏み出しました。

今回は、アプリのリニューアルをすすめていく中で、試してみたことについて、お話しします。

アプリリニューアルの内容やそのデザイン意図については、13年続いた「つくれぽ」をリニューアルした話|Misaki Kubosaka|noteが詳しいので、こちらをお読みください。

リニューアルプロジェクト第1フェーズの問題点

iOSアプリのリニューアルプロジェクトは、とても大きく、機能を段階的にリリースするため、3つのフェーズに分けて開発していくことが決まっていました。 そのため、開発チームはメンバーの追加をしつつ、複数回の開発サイクル(仕様決定、設計、実装、検証)を繰り返すことになりました。

クックパッドのサービス開発では、主に、ディレクターとデザイナーが企画と仕様決めを行い、エンジニアが実装し、ディレクター/デザイナーが作成するテストケースを元に検証を行うという方法が取られます。 以後の「ディレクター」は、企画と仕様決めに責任を持つディレクターとデザイナーの両方を指します。

私が参加したのは第1フェーズの途中からで、そこではテストケース作成をはじめとした検証を担当しました。 第1フェーズでは、 テスト期間の開始間際まで仕様の整理が行われていたり、不明瞭になっていた箇所への仕様追加がされたりしていました。 どうにか第1フェーズの開発を終え、リリースすることはできましたが、仕様が不明瞭なまま開発をすすめていくことに大きな不安を感じました。

その次の第2フェーズでは、私は開発エンジニアとしてモバイルアプリ開発をすることにしていました。 第1フェーズの反省から、いかに仕様の決定を早期に行うかを考え、仕様の抜けを早く検知する手段を講じる必要がありました。

第1フェーズのすすめ方の問題点はいくつかありました。

  • ディレクターが仕様を決めるが、複雑なユーザー状態すべてを考慮できなかった
  • 影響範囲が大きいため、たくさんの仕様の検討会が行われており、ディレクターが結論を精査する時間がとれなかった
  • どこまで決まっているかの管理をディレクター任せにしてしまったことで、ごく少数の人間だけが仕様を知っている状態が発生した
  • 実装担当は共有された情報から仕様を理解していたが、細かな認識の齟齬があることに後々まで気づけなかった
  • 後々発覚した認識の齟齬を埋めるために、仕様追加がされ、開発スケジュールがずるずると伸びた

仕様を実現していく上で必要な情報共有が不足している、ディレクターから実装者への一方通行であることが問題と考えました。

そこで、仕様の不明瞭な点を実装開始前に明かにする、そのために情報共有の精度をあげる方法を探すことにしました。

テストケースの作成を通じて、仕様を詳細化する

仕様の情報共有の精度をあげる方法を2案考えました。

  1. ディレクターに仕様詳細化をお願いし、その共有を実装担当者とする時間を設ける
    • 第1フェーズでは、やっているつもりだができていない状態だった
    • タイトなスケジュールの中では、実現可能性を考慮してセカンドプランを選択するべき場面があるが、それをディレクターだけでは判断できない
  2. ディレクターと実装担当者が会話した上で、実装担当者が仕様を詳細化する
    • 仕様の検査に確実に2者の視点が入るので、情報共有の不足を低減できる
    • 実装担当者のシリアルタスクのため、実装前に詳細化を完了しやすくなり、その結果、仕様追加の追跡がしやすく無理な変更の抑制ができる

案1は、第1フェーズで結果的にうまくいかなかった方法とあまり差がないこと、また、実装のコストや難易度の反映が遅くなる危険があったため、案2を採用しました。

私が属した機能グループでこの取り組みを行いました。大きく2つの機能を実装するグループです。

新機能に対して、ディレクターと実装担当者間で仕様の共有会を行い、そのインプットを元に実装担当者が仕様を詳細化しました。 このとき、ユーザー状態や利用状況の網羅性をあげるため、分析が網羅的になるようテストケースを作成するという手段をとりました。

テストケースというフォーマット

テストケースは、一般に、状況設定と操作と期待結果の組み合わせを列挙するものです。 とある機能において取り得る状況を網羅するには、テストケースの状況設定を細かく分析できるかが鍵です。 状況設定を細かく分析しやすいと考えたので、テストケースのフォーマットを利用することにしました。

そのとき使用したフォーマットは以下のものです。

f:id:ichiko_revjune:20200316114550j:plain
テストケースのテンプレート

一部は選択式にしつつ、その他のデータやユーザーの条件は自由設定にしています。 これまでの経験から仕様に現れない操作は忘れられたがちなため、操作は選択式にしました。

例えば、画面の通信エラーが起きたときの振舞いは未定義になりがちなので「通信エラー」、文字入力に関するバリデーションエラーの扱いを考慮してもらうために「文字入力」などを用意しました。 他には、細かな機能の出し分けがあるため、ユーザーステータスも選択式にしました。

仕様詳細化の効果

実装担当者は、テストケース作成を通じて、不明点/検討されていない点をリストアップしました。それをディレクター、デザイナーと共に解消した上で、実装を開始しました。

成果物としたテストケースは、その後ディレクターが加筆して検証フェーズで利用されました。 加筆といっても、この取り組みをしたのは新機能まわりだけだったので、ディレクターが新規に作成した既存機能との関連を見るテストケースの方が圧倒的に多いです。

検証フェーズでは、実装担当者が作成したテストケースの周辺では、ディレクターの追加したテストケースによって、大きな仕様差異が発覚することはありませんでした。

仕様詳細化をする段階で、問題を発見できたことで、第1フェーズに比べて安定した進行でリリースまで漕ぎ着けることができました。

リリース後の振り返りでは、エンジニアからは「実装開始前にテストケースができていたことで、仕様の不明点が洗い出せてよかった」と高評価を得ました。 一方、ディレクター陣からは、エンジニアが作ったテストケースにディレクターが手を入れるという形をとったため、検証の信頼性が低いので今後はディレクターがテストケースを作る、という評価を得ました。

実装担当者がテストケースを作成する是非

一般的に、実装担当者が作成するテストケースでは見つけられない不具合が多い、という信頼性の低さがあります。 テストケースが先でも、実装が先でも、先に考えた理解の範囲に引きずられて、網羅性が上がらないことは想像しやすいと思います。

ディレクター陣からの評価が下がった原因も、「実装担当者が作成したテストケースを検証に使った」ことにあると考えられます。

詳細化の分析手段としてテストケース作成のフォーマットに載せたことで、仕様の検証精度は上がった可能性はありました。 実装担当者からの反応はよかったので、仕様を理解する、過不足なく機能を実現することには貢献したと考えられます。

しかし、その成果物を流用させてしまったことで、 検証能力が低いテストケースであるために不具合件数が減ったのか、 実装精度が高いことで不具合件数が減ったのか、の区別ができなくなっていました。

私が検証のためのテストケース作成と完全に分離せず、成果物の流用を許容してしまったことで、有用性の評価が十分にできなかったのは残念でした。 あくまで詳細化の過程の成果物であり、検証用途ではないとするべきでした。

この手の信頼性問題は、コンセンサスを得ていない手段を使ったことで結果が下がった可能性をゼロにできなければ、マイナスに取られる他ないので致し方ないと思っています。

反省点はありますが、「仕様を考えている人以外も混じえて、実装開始前に仕様を明かにする」ことで、スムーズに開発をすすめることはできました。

次回は、成果物が一人歩きしても問題ないよう、用途を制限することと、誤解の生じる利用のされ方を回避することが必要になるでしょう。 分析の助けになることが重要のため、成果物の形をテストケース以外にできると、外部からの誤解も減らせると思えるので、他の形を模索したいと思います。

クックパッドではモバイルアプリの品質を安定させたいiOS/Androidエンジニアを募集しています。 https://info.cookpad.com/careers/

スプリングインターンシップをオンラインで開催致します!

こんにちは、レシピ事業開発部の赤松( @ukstudio )です。

毎年恒例となっている春のインターンシップですが、今年も開催致します!新型コロナウィルスの影響をふまえ、オンラインで実施することに致しました。当日はお手持ちのマシンからZoomで参加頂く予定です。

大規模トラフィックを支える技術

今年はエンジニア向けに1コース用意しました。題しまして「春ダッシュスペシャル 大規模トラフィックをさばくアプリケーションのパフォーマンスチューニングを学ぼう!」です!以下の日程・場所で開催致します。

  • 開催日: 4月29日(水・祝) 13時〜17時
  • 開催場所: オンライン(Zoom)

クックパッドは現在74カ国/地域・32言語に展開しており、月間の利用者数も約9,300万人にのぼる大規模サービスです。ユーザーからのアクセスだけでもピーク時には秒間数千アクセスにも達します。

このコースではこのような大規模トラフィックを支えるための技術を実践を通して学ぶことができる内容となっています。大規模トラフィックに関する技術の経験は個人で得るにはなかなか難しい部分もあると思いますので、この機会にぜひ体験してみてください。

以下のインターンシップ特別サイトからご応募頂けます。

internship.cookpad.com

オンラインでの開催について

当日は講師がZoomで画面共有をしながら講義を進めることになります。実はこの形式だと、オフラインの時と比べてスクリーンが見やすい、声が聞きやすい部分もあります。

一方でオンライン開催だと質問がしづらいんじゃないか、講義についていけなかったら置いてけぼりになるんじゃないかという不安があるかもしれません。当日はメインで話す講師とは別にTAも用意しております。TAがSlackでのサポートや、場合によっては1対1でのZoom MTGでサポートします。

実際に既にオンラインでの勉強会やイベントを弊社で行なっていますが、スプリングインターンもオンラインでできる!という手応えを感じています。ぜひオンラインというところに気後れせずに応募して頂けたらなと思います。

デザイナー向け UIデザインとサービス開発

デザイナー向けにも1コース用意しています。こちらは「クックパッド流!UIデザインをFigmaで体験しよう」とFigmaを用いたUIデザインとサービス開発の基礎を体験することができるコースです。デザイナーの方はぜひこちらにお申し込みください。詳細については後日noteの方に記事が公開される予定なので、そちらを見てもらえればと思います。

note.com

  • 開催日: 4月25日(土) 13時〜17時
  • 開催場所: オンライン(Zoom)

応募締切は4月6日

エンジニア向けもデザイナー向けも応募の締め切りは4月6日までとなっております。みなさまのご応募お待ちしております!

internship.cookpad.com

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/