GCP で Google Calendar を CalDAV サーバに複製する


概要

Python バッチスクリプトで Google Calendar からスケジュール情報を採取し、CalDAV サーバへ複製する例を紹介します。

  • GSuite 上の任意のユーザの Calendar に、Google Calendar API を介してアクセスする
  • GCP の Cloud Functions にデプロイした Pythonバッチスクリプトを、Cloud Scheduler + Pub/Sub で定時実行する
  • APIアクセス認証をGCPのサービスアカウントキーで行い、個々のカレンダーオーナーからのアクセス許可は不要とする

サンプルコード

構成

グレーは環境設定、ブルーはバッチ処理です。

1. GSuiteでAPIアクセスを許可する

アクセス対象としたいカレンダーが存在する GSuite 環境に対して、外部からのAPIアクセスを受付可能に設定をします。公式ドキュメントの、管理コンソールで API アクセスを有効にする 手順になります。

APIリファレンスを有効にする

APIアクセスを有効にする

2. GCPにサービスアカウントを作成しキーを得る

今回は サービスアカウントキー を用いた認証ストラテジで Calendar API を利用します。なお、サービスアカウントではなくパーソナルなアカウントの認証ストラテジでAPIを利用する方式もありますが、当記事では対象外です。認証ストラテジの選択肢と差の解説は、こちらの公式ドキュメントが分かりよいと思います。

IAMと管理 > サービスアカウント でサービスアカウントを作成します。

サービスアカウントの鍵を生成し、JSON形式で取得します。鍵は、バッチスクリプトがAPIアクセス認証時に使います。


サービスアカウントのクライアントIDを確認します。クライアントIDは、バッチスクリプトの認証情報および、GSuiteのAPIクライアントアクセス許可対象として使います。

3. GSuiteでAPIクライアントアクセスを許可する

GSuite で、どのリソースに対するAPIアクセス操作を、誰に許可するかを設定します。設定手順は、OAuth: API クライアント アクセスの管理 になります。

設定内容は、クライアント名 には先にGCP上で作成したサービスアカウントの クライアントID を指定し、
スコープ には https://www.googleapis.com/auth/calendar.readonly, https://www.googleapis.com/auth/calendar.events.readonly を指定して承認します。

余談ですが、指定可能なスコープとAPIの仕様は APIリファレンス に書かれています。

4. CalDAV サーバを立てる

当サンプルでは、CalDAV サーバである Radicale に、Google Calendar から採取した情報を吐き出して、ユーザに利用させます。試験版のRadicaleは dockerコンテナ で立てるのが手っ取り早いです。

起動するとこんな感じです。サーバのURLがアプリに与える環境変数の CALDAV_SERVER_URL、 ログイン情報が CALDAV_SERVER_USER, CALDAV_SERVER_PASS です。

ログインすると、即カレンダーの作成画面になりますので、こんな感じで作ります。Titleがカレンダー名であり、アプリに与える環境変数の CALDAV_SERVER_CALENDAR_NAME です。

作成したカレンダーのURLは、http://${HOSTNAME}:5232/${USERNAME}/${CALENDAR_ID}/ になり、環境変数 CALDAV_SERVER_URL に相当します。

  • 余談
    • CalDAV サーバは、例えばThunderbirdのカレンダー拡張(Lightning)のようなクライアントから参照・同期できる、ネットワークカレンダーサーバの規格です。RFC 4791で仕様が定義されており、いわゆるiCAL形式(RFC 2445) でデータ交換をします。
    • そもそもカレンダーというモノは、割と面倒くさい概念(タイムゾーン、Free/Busy、参加者と参加不参加、繰り返しタスクなど)がありますので、車輪があるなら再発明しないで利用するのがお得です。

5. GCP上にバッチ処理を設置する

Pub/Subの作成

トピックを作成し、トピックの名前を確認しておきます。


作成したトピックに対する、Pullタイプのサブスクリプションを作成します。サブスクライバとなるCloudFunction宛のメッセージングを定義する事に相当します。


Cloud Scheduleの登録

先ほど作成したPub/Subトピックをターゲットとするスケジュールを作成します。スケジュールが、Pub/Subに対するパブリッシャとなる事に相当します。

作成したら、試しにジョブを実行して Publish メッセージングが成功する事を確認しておきます。

Pythonスクリプトのデプロイ準備をする

Cloud Storageに、非公開バケット function-codes を1つ作ります。

次に、スクリプトcredentials.json を、先の手順で取得したサービスアカウントのJSON形式の鍵に差し替えて、Pipfile, Pipfile.lock, main.pyおよび、差し替え後の credential.json を1つのZipファイル gcalendar-fetcher.zip に圧縮し、先に作成したCloud Storageのバケットにアップロードします。

PythonスクリプトをFunctionsにデプロイする

バケット内のZipファイル gcalendar-fetcher.zip をソースとする関数を定義します。トリガーには、Pub/Sub に作成したトピック test-getting-gcal-events トピックを指定します。実行する関数には main関数 を指定します。

また、環境変数は こちらに定義した変数 を与えます。

6. 実行して結果を確認してみる

Cloud Scheduler に作成したタスクが実行済の場合は結果欄に「成功」が出ていると思います。まだスケジュールが走っていない場合は、実行ボタンを押下し発火させた結果を同様に確認します。

また、Function にデプロイしたスクリプトが、Pub/Sub メッセージングを介して実行されていることも確認します。

いずれも成功なら、CalDAVサーバに登録されたはずのカレンダー情報を閲覧してみます。radicale から ical をダウンロードして中身を確認してみましょう。icalフォーマットのデータが得られます。

BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//PYVOBJECT//NONSGML Version 1//EN
X-WR-CALDESC;VALUE=TEXT:test
X-WR-CALNAME;VALUE=TEXT:test
BEGIN:VEVENT
UID:[email protected]
DTSTART:20190913T013000Z
DTEND:20190913T084500Z
DTSTAMP:20190726T050838Z
LAST-MODIFIED:20190726T050856Z
STATUS:CONFIRMED
SUMMARY:展示会にいく
TRANSP:OPAQUE
URL:https://www.google.com/calendar/event?eid=XXXXXXXXXXXXXXXXXXXXXXXX
END:VEVENT
BEGIN:VEVENT
UID:[email protected]
DTSTART:20191004T040000Z
...
END:VEVENT
END:VCALENDAR

ThunderbirdのLightning拡張でネットワークカレンダーとして見る方法もあります。

環境変数 CALDAV_SERVER_URL に与えたURLが、ネットワークカレンダーの場所に相当します。

よく採れてます、使えそうですね。
空き時間調整くんとか、あなた会議長杉くんとかを作ってみるつもりです。

追記

生のクレデンシャルファイルをデプロイソースに含めて保管しておくのが怖いので、クレデンシャルを暗号化して取り扱うコード修正を加えてみました。Before/After形式の続き記事の体裁で書いたのでどうぞ。