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

# 概要

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

* GSuite 上の任意のユーザの Calendar に、Google Calendar API を介してアクセスする
* GCP の Cloud Functions にデプロイした Pythonバッチスクリプトを、Cloud Scheduler + Pub/Sub で定時実行する
* APIアクセス認証をGCPの[サービスアカウント](https://cloud.google.com/iam/docs/service-accounts?hl=ja)キーで行い、個々のカレンダーオーナーからのアクセス許可は不要とする

**サンプルコード**

https://github.com/coleyon/gcalapi-caldav-sample

**構成**

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/626c0e4b-69da-999d-e589-aace2018d30f.png)

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

アクセス対象としたいカレンダーが存在する GSuite 環境に対して、外部からのAPIアクセスを受付可能に設定をします。公式ドキュメントの、[管理コンソールで API アクセスを有効にする](https://support.google.com/a/answer/60757?hl=ja) 手順になります。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/27def7da-8634-ca8b-da8f-b39f9b947407.png)

APIアクセスを有効にする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/fe9d6688-8eb4-7470-c909-6fef995a59ca.png)


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

今回は `サービスアカウントキー` を用いた認証ストラテジで Calendar API を利用します。なお、サービスアカウントではなくパーソナルなアカウントの認証ストラテジでAPIを利用する方式もありますが、当記事では対象外です。認証ストラテジの選択肢と差の解説は、[こちらの公式ドキュメント](https://cloud.google.com/docs/authentication/?hl=ja#authentication_strategies)が分かりよいと思います。


`IAMと管理` > `サービスアカウント` でサービスアカウントを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/df98b166-9d11-419b-980f-e582bcacf371.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/125b0ba8-ed9d-112f-fbe4-3695f7f15951.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/aa045674-0991-6f19-0afc-425a7f5cfbb5.png)


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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/4cc2d142-0cc8-3333-f0c8-5b07a7507601.png)


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

GSuite で、どのリソースに対するAPIアクセス操作を、誰に許可するかを設定します。設定手順は、[OAuth: API クライアント アクセスの管理](https://support.google.com/a/answer/162106?hl=ja) になります。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/ec3c5502-ff78-a3ba-ea6c-77401101cd3a.png)

余談ですが、指定可能なスコープとAPIの仕様は [APIリファレンス](https://developers.google.com/identity/protocols/googlescopes#calendarv3) に書かれています。


# 4. CalDAV サーバを立てる

当サンプルでは、CalDAV サーバである [Radicale](https://radicale.org/) に、Google Calendar から採取した情報を吐き出して、ユーザに利用させます。試験版のRadicaleは [dockerコンテナ](https://hub.docker.com/search?q=radicale&type=image) で立てるのが手っ取り早いです。

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/fc0e0094-7520-8e61-5cb8-46b1f115a4f7.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/f2a8fdc7-6e3b-f279-093b-867a68404881.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/9d10e060-b185-6fbf-b9cc-b382e132e329.png)


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


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


**Pub/Subの作成**

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/e9b734e8-613a-7322-7f43-19ff747b0e87.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/489326cb-cfa7-754e-045f-28e0beeb5610.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/b137ce10-c591-f112-6940-4869b03991a9.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/0597e252-0316-7a18-24ee-6a49e8067169.png)


**Cloud Scheduleの登録**

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/38233d76-0055-91e7-da3b-89e893bd72cb.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/a7c450d8-5c68-bd82-e3f0-8dfa9017ad1f.png)


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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/5b54ce32-62a3-dca6-abc9-e1544cb2d92f.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/ca1c2737-6a65-d087-afd1-e2d8213eead6.png)


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

バケット内のZipファイル `gcalendar-fetcher.zip` をソースとする関数を定義します。トリガーには、Pub/Sub に作成したトピック `test-getting-gcal-events` トピックを指定します。実行する関数には [main関数](https://github.com/coleyon/gcalapi-caldav-sample/blob/master/main.py#L124) を指定します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/d35172e9-d1f2-0a98-bf31-6a85b7614182.png)

また、環境変数は [こちらに定義した変数](https://github.com/coleyon/gcalapi-caldav-sample#environment-variables) を与えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/05360dab-b703-38a5-1729-6dda65d35ccd.png)


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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/0a168fa7-3ada-ba24-ed5f-36e62e53f79b.png)


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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/9eb83c24-2f75-35e7-2bb8-c300a55404c2.png)

いずれも成功なら、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:xxxxxxxxxxxxxxxxxxx@google.com
    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:yyyyyyyyyyyyyyyyyyyyyyyyyy@google.com
    DTSTART:20191004T040000Z
    ...
    END:VEVENT
    END:VCALENDAR


ThunderbirdのLightning拡張でネットワークカレンダーとして見る方法もあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/a296f347-2b24-7378-b7c4-e78da8453d0a.png)
環境変数 `CALDAV_SERVER_URL` に与えたURLが、ネットワークカレンダーの場所に相当します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/2134e01d-4f7e-73eb-6ff2-f1b9118e2a2b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157638/5db9d351-94c8-143d-5683-edfe6fcaa212.png)

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


# 追記

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

https://qiita.com/coleyon/items/9fa0a668aff3e4cc4c2c