作ったもの
Googleカレンダーに予定を作成/変更/削除すると、PagerDutyのメンテナンスウィンドウが作成/変更/削除されます👍
既存のAppではScheduleをGoogleカレンダーに出力することしかできないので作成しました。たぶん先行事例はありません。
ref: Schedules in Third-Party Apps
コード(全文)
↓をクリックすると開きます
// スクリプトプロパティ(Key-Valueストア)を使う
const db = PropertiesService.getScriptProperties();
// 定数
const PD_TOKEN = 'Token token=' + db.getProperty('PD_TOKEN');
const PD_HEADERS = {
"Accept": "application/vnd.pagerduty+json;version=2",
"Authorization": PD_TOKEN
}
const PD_SERVICE_IDS = [
{ "id": "PVG3BVI", "type": "service_reference" }, // service-A
{ "id": "PHN2FVI", "type": "service_reference" } // service-B
]
const PD_API = 'https://api.pagerduty.com'
// メイン関数
function calendarUpdated(e) {
let options = {
'showDeleted': true,
'maxResults': 100
};
// syncTokenを読み込む
const syncToken = db.getProperty('SYNC_TOKEN');
if (syncToken) {
options.syncToken = syncToken;
} else {
// syncTokenがない場合(初回)、過去7日間の予定を対象にする
options.timeMin = getDate7daysAgo();
}
// 予定の取得
const events = Calendar.Events.list(e.calendarId, options);
for (const event of events.items) {
// eventIdに紐づくmaintenanceWindowIdを読み込む
let maintenanceWindowId = db.getProperty(event.id);
// maintenanceWindowIdがnullならCreate
// event.statusがcancelledならDelete
// それ以外はUpdate
if (maintenanceWindowId == null) {
maintenanceWindowId = CreateMaintenanceWindow(event.start.dateTime, event.end.dateTime, event.summary);
db.setProperty(event.id, maintenanceWindowId);
} else if (event.status == 'cancelled') {
DeleteMaintenanceWindow(maintenanceWindowId);
db.deleteProperty(event.id);
} else {
UpdateMaintenanceWindow(maintenanceWindowId, event.start.dateTime, event.end.dateTime, event.summary);
}
}
// syncTokenを保存する
db.setProperty('SYNC_TOKEN', events.nextSyncToken);
}
// Create
function CreateMaintenanceWindow(startTime, endTime, description) {
const data = {
"maintenance_window": {
"type": "maintenance_window",
"start_time": startTime,
"end_time": endTime,
"description": description,
"services": PD_SERVICE_IDS
}
};
const options = {
'method': 'post',
'contentType': 'application/json',
'headers': PD_HEADERS,
'payload': JSON.stringify(data)
};
const url = PD_API + '/maintenance_windows';
const response = UrlFetchApp.fetch(url, options);
const responseJSON = JSON.parse(response.getContentText());
return responseJSON.maintenance_window.id;
}
// Delete
function DeleteMaintenanceWindow(id) {
const options = {
'method': 'delete',
'headers': PD_HEADERS,
};
const url = PD_API + '/maintenance_windows/' + id;
UrlFetchApp.fetch(url, options);
}
// Update
function UpdateMaintenanceWindow(id, startTime, endTime, description) {
const data = {
"maintenance_window": {
"type": "maintenance_window",
"start_time": startTime,
"end_time": endTime,
"description": description,
"services": PD_SERVICE_IDS
}
};
const options = {
'method': 'put',
'contentType': 'application/json',
'headers': PD_HEADERS,
'payload': JSON.stringify(data)
};
const url = PD_API + '/maintenance_windows/' + id;
UrlFetchApp.fetch(url, options);
}
// 7日前の日付を返す関数
function getDate7daysAgo() {
const date = new Date();
date.setDate(date.getDate() - 7);
return date.toISOString();
}
コード以外で必要な作業
先にGoogle Cloud Platformのコンソールで次の作業をします。
- 適当な名前でGCPプロジェクトを作成する
-
APIとサービス
→APIとサービスの有効化
で「Google Calendar API」を有効にする -
APIとサービス
→OAuth 同意画面
で同意画面を新規作成する。スコープは「すべてのカレンダーの予定を表示」
次にGoogle Apps Script(以下、GAS)で次の作業をします。
-
プロジェクトの設定
で先ほどのGCPプロジェクトを紐付ける -
エディター
→サービスを追加
で「Google Calendar API」を有効にする -
トリガー
→トリガーの追加
でイベントソースに「カレンダーから」を選ぶ。連携するGoogleカレンダーのメールアドレスを入れ、遷移先でOAuth認証を行う
解説
1. なぜGASなのか?
まずはノーコードで実現できないか調べました。IFTTTには「予定の追加」というトリガーがありますが、変更と削除はなし。
Zapierには変更と削除もありました。
ただしZapierは無料プランだと毎月100タスクしか実行できません。開発で試行錯誤していたら100回はあっという間に到達しそうなのでやめました。が、この記事を書くために再度調べたら「最初の2週間は1000タスクを実行できる」だと!? Oh...
ref: Get started with your free Zapier trial
コードを書くなら、簡単にGoogleカレンダーと連携できるGASはとても便利です。学習コストや運用コストも低いです。
2. GASでGoogleカレンダーをトリガーにするときの概要
Advanced Calendar ServiceはGASでGoogle Calendar APIを使うためのサービスです。前述の通りGASのエディター
→サービスを追加
から有効にできます。
予定をハンドリングする方法は下記の記事がとても参考になりました。
追加/変更/削除された予定を取得するにはsyncToken
をリクエストに含めてAPIを叩きます。
このsyncToken
はAPIを叩くとnextSyncToken
という名前でレスポンスに入ってくるものです。
初回のみsyncToken
を使わずにAPIを叩いてnextSyncToken
をスクリプトプロパティに保存し、2回目以降はそれを使ってAPIを叩き、また返ってきたnextSyncToken
を保存する、という流れにしています。
3. スクリプトプロパティ
スクリプトプロパティはGASで使えるKey-Valueストアの1つです。簡単かつ便利なので、ついつい雑に使ってしまいます。
const db = PropertiesService.getScriptProperties();
// READ
const syncToken = db.getProperty('SYNC_TOKEN');
// WRITE
db.setProperty('SYNC_TOKEN', events.nextSyncToken);
4. 予定とメンテナンスウィンドウの紐付けを保存する
APIについては後述しますが、変更と削除はメンテナンスウィンドウのIDを指定してAPIを叩く必要があります。
そのため、メンテナンスウィンドウの作成後にGoogleカレンダーの予定のID(event.id
)とメンテナンスウィンドウのID(maintenanceWindowId
)の紐付けを保存しておく必要があります。
そこでevent.id
をキー、maintenanceWindowId
を値にしてスクリプトプロパティに保存しました。
メインのロジックは次のとおりです。
// 予定に紐づくメンテナンスウィンドウのIDを取得する
// 新しい予定の場合、まだ存在しないのでnullが返る
let maintenanceWindowId = db.getProperty(event.id);
if (maintenanceWindowId == null) {
maintenanceWindowId = CreateMaintenanceWindow(引数省略); // 新規作成
db.setProperty(event.id, maintenanceWindowId); // 作成されたメンテナンスウィンドウのIDを保存する
} else if (event.status == 'cancelled') {
DeleteMaintenanceWindow(maintenanceWindowId); // 削除
db.deleteProperty(event.id); // 保存されていたIDも削除する
} else {
UpdateMaintenanceWindow(maintenanceWindowId, 以下引数省略); // 変更
}
5. PagerDuty APIを叩く
API Referenceが使いやすくて神です。
メンテナンスウィンドウのCreate(POST)、Update(PUT)、Delete(DELETE)をそれぞれの関数で叩いています。
GASで外部APIを叩く際はUrlFetchApp.fetch(url, options)
を使うと便利です。
例えばUpdateの関数は次のようになります。
function UpdateMaintenanceWindow(id, startTime, endTime, description) {
const data = {
"maintenance_window": {
"type": "maintenance_window",
"start_time": startTime,
"end_time": endTime,
"description": description,
"services": PD_SERVICE_IDS
}
};
const options = {
'method': 'put',
'contentType': 'application/json',
'headers': PD_HEADERS,
'payload': JSON.stringify(data)
};
const url = PD_API + '/maintenance_windows/' + id;
UrlFetchApp.fetch(url, options);
}
課題と今後の展望
- エラーハンドリング
- スクリプトプロパティに古いデータが溜まっていく問題
-
PD_SERVICE_IDS
に対象にするサービスを書いたが、スプレッドシートから読み込んでも良さそう - 複数のチームで使えるようにしたい(複数のカレンダーへの対応・予定のタイトルや本文に応じた処理の分岐)
- ScheduleのOverrideも作れるようにしたい!!(オンコール担当者の上書き)