SREの@biosugar0です。 SREチームでは、スマートマットクラウド、スマートマットライトの信頼性を維持、向上するために2つのプロダクトを横断してシステムの安定性と開発者体験の向上に取り組んでいます。 今回は、最近始めたAsanaを用いたSREの運用負荷の計測とトラッキングについて紹介します。
SREがサイト信頼性エンジニアリングに取り組んでいく際に重要な概念のひとつに、 トイル があります。 トイルとはSREが日々行うタスクのうち、手作業であり、繰り返され、自動化が可能であり、計画されたものではなく割り込みで始まり、長期的な価値を産まず、サービスの成長に対して比例して追加で作業が必要になるようなタスクを指します。
SREの発祥のGoogleと同じく、弊社のSREチームでも運用業務(つまりはトイル)を、各人の作業時間の50%以下に抑えるという原則が掲げられています。 SREチームのメンバーの作業時間のうち、最低50%はトイルの削減かエンジニアリングプロジェクトの作業に使用するためです。
この原則が重要なのは、トイルというものはプロダクトの成長と共に増えていく特性があるので、削減せずに放置してしまえばプロダクトが成長を続けている限り急速にSREの時間を食いつぶし、SREは割り込みの依頼のようなトイルだけをこなす組織になってしまうからです。そしてSREの人数は急速に増えることはないため、プロダクトの成長のボトルネックとなってしまいます。
継続的にトイルを減らすことで、SREの作業時間をスケーラビリティの向上、次世代のプロダクトアーキテクチャ設計、SREのチーム間で利用できるツール群の構築などに注力させることができるようになります。
継続的なトイルの削減のために、まずはどのくらいの作業時間をトイルに使用しているかを計測する必要があります。 弊社では、厳密にこのタスクはトイルなのか?を見ていくと「トイルなのか判断するためのトイル」が発生し、逆にSREメンバーの負担になるという判断から、 SREプロジェクトとオーバーヘッド(サービスを稼働させることに直結していない管理的な作業。例えばミーティング、評価、採用など)以外の運用系業務全てを「トイル、運用」とラベル付けして計測することにしました。
SREチームではAsanaによるタスク管理をしているため、計測にはAsanaのタスクを使います。 Asanaにはタスクの作成日時やステータスの日時が記録されるため、そこからタスク完了までの時間を計算することも考えたのですが、 あくまでSREチームがプロジェクトに時間を使えるようにするための計測なので、そこまで厳密に計測することはせず、ざっくりと使った時間をタスク完了時に選択してもらう方法にしました。(作業開始時ピッタリにステータスを変更するのも大変ですしね。)
「作業の種類」、「運用作業に使用した時間」というカスタムフィールドを用意して運用業務発生時に「作業の種類」に「トイル、運用」を付与してタスクを作成し、完了後にざっくりと使った時間を、「運用作業に使用した時間」の選択肢から選ぶ流れになっています。
上記で紹介した方法でタスクごとの運用に使用した時間は記録できるようになりました。 次は各メンバーおよびSREチーム全体でどのくらい運用に時間を使っているかを集計します。 これにはAsanaのAPIとGoogle Apps Script(TypeScript)、Google スプレッドシートを使用しました。 AsanaのAPIを使用して使用時間を集計するスクリプトを週次で実行しています。
AsanaのAPIを使うには、まずトークンが必要です。今回は個人アクセストークンを使用しています。 右上の自分のアイコンから設定→アプリ→デベロッパーアプリを管理と選択し、個人アクセストークンを取得しておきます。
APIの仕様については公式のドキュメント が詳しいのですが、
タスクの情報の取得は、GET /projects/{project_gid}/tasks
エンドポイントを使用しました。以下のように使っています。
const project = "xxxxxxxxxxxxxxxx";
const optFields = [
"name",
"assignee.name",
"notes",
"due_on",
"modified_at",
"completed",
"completed_at",
"custom_fields.enum_value",
"custom_fields.name",
];
const url = `https://app.asana.com/api/1.0/projects/${project}/tasks?opt_fields=${optFields.join( ",")}`;
const response = UrlFetchApp.fetch(url, options);
const jobj = JSON.parse(response.getContentText());
if (!jobj) {
return [];
}
const result = jobj as AsanaTaskResponses;
JSONで返ってきたレスポンスは以下のように定義した型に流し込んでします。
export type AsanaTaskResponses = {
data: AsanaTaskResponse[];
};
type AsanaTaskResponse = {
gid: string;
assignee: {
gid: string;
name: string;
} | null;
completed: boolean;
completed_at: string | null;
custom_fields: [
{
gid: string;
name: string;
enum_value: {
gid: string;
name: string;
} | null;
}
];
name: string;
notes: string;
};
これでタスク情報のリストが取れました。
タスクの配列は取れたのですが、これでは完了したトイル以外のタスクも混ざった状態です。集計の前に、完了したトイルを抽出しなければなりません。for文でループさせて対象の配列を作るために、以下のように抽出、生成を行います。 イメージとしてTypeScriptのfor文の中の記述例も併記します。
タスクのオブジェクトが持つ completed_at
が null
かどうかで判別します。完了したタスク以外は無視します。
if (sreTask.completed_at == null) continue;
これには、タスクのオブジェクトが持つカスタムフィールドの情報の配列が入っている custom_fields
を使います。
カスタムフィールドのオブジェクトcustom
の enum_value.gid
を見ることで判別します。
for (const custom of sreTask.custom_fields) {
if (custom.enum_value === null) continue;
toil = custom.enum_value.gid === "xxxxxxxxx"; // カスタムフィールド"作業の種類" が "トイル、運用"
上の例では、カスタムフィールド"作業の種類" が "トイル、運用"の場合、変数 toil
がtrueになるようになっています。
"トイル、運用"のgidはハードコードして判定しているのですが、この値はAPIクライアントやcurlでAPIを叩くとレスポンスに以下のような項目があるのでそれを用います。
{
"gid": "xxxxxxxxx",
"enum_value": {
"gid": "xxxxxxxxxxxx",
"color": "red",
"enabled": true,
"name": "トイル、運用",
"resource_type": "enum_option"
},
カスタムフィールド"運用作業に使用した時間"に設定された時間を集計のために時間換算した数値として取得します。
これもまた、事前にAPIを叩いてカスタムフィールド"運用作業に使用した時間"のgid
および使用した時間の選択肢の enum_value.gid
を調査、取得しておき、対応のための定数を定義しておきました。
時間のマッピング用の定数を定義しておき、
const ToilLevel: { [gid: string]: number } = {
"xxxxxxxxxx": 0.25,
"xxxxxxxxxx": 0.5,
"xxxxxxxxxx": 1,
"xxxxxxxxxx": 2,
"xxxxxxxxxx": 3,
"xxxxxxxxxx": 5,
"xxxxxxxxxx": 8,
"xxxxxxxxxx": 13,
"xxxxxxxxxx": 21,
"xxxxxxxxxx": 34,
"xxxxxxxxxx": 40,
};
gidから使用した時間の数値を取得
for (const custom of sreTask.custom_fields) {
// ...
if (custom.gid !== "xxxxx") continue; // "運用作業に使用した時間"以外はスキップ
if (custom.enum_value.gid in ToilLevel) {
toilHour = ToilLevel[custom.enum_value.gid]; // 選択肢のgidから数値を取得
}
メンバーごとにトイルに使用した時間を集計するために、タスクにメンバーがアサインされている必要があります。
const assignee = sreTask.assignee;
if (!assignee) continue; // 誰もアサインされていなかったらスキップ
集計に使用するタスクの情報の型も用意しておきます。
export type AsanaTask = {
name: string;
completed: boolean;
completedAt: Date;
gid: string;
timeSpent: number;
assignee: string;
note: string;
};
条件を通り抜けたタスクの情報をトラッキングするタスクの配列に格納します。
const trackingTask: AsanaTask = {
assignee: assignee.name,
name: sreTask.name,
gid: sreTask.gid,
completed: sreTask.completed,
completedAt: completedAt,
timeSpent: toilHour,
note: sreTask.notes,
};
trackingTasks.push(trackingTask);
上記まででトイルの情報の配列が取得できました。次はそれを集計します。 記録後のイメージは以下のようになります。メンバーごと、チーム全体でのトイル比率を日次の表にしているのと、折れ線グラフの生成、更新も行っています。
処理の細かい内容については割愛させていただきますが、 以下のようなことをしています。
このスクリプトをGoogle Apps Scriptの定期実行機能によって毎週土曜日に実行することで運用負荷をトラッキングできるようにしています。
SREチームの運用負荷が計測できるようになったので、次はこれを用いて負荷が上がってきたらトイル削減タスクの優先度を上げるなどの判断に使用しようと考えています。 まだ粗もあると思うので、改善しつつプロジェクトとトイルのバランスをデータ駆動にとれるチームを目指して運用していきます。
SREチームでは、他にも様々な信頼性向上のための取り組みを行っています。 やりたいことは山積みで絶賛募集中です! 興味のある方はぜひ以下からご応募ください。
https://open.talentio.com/r/1/c/smartshopping/pages/20728