認証ファイルを発行せずに、Google Compute Engineからgoogleカレンダーへアクセスする

# はじめに
googleカレンダーなどのgoogleサービスにアクセスするときは、サービスアカウントのjsonやAPIキーなどの認証ファイルを準備するよう公式のリファレンスで示しています。
しかし、GCPの計算リソースとサービスアカウントとアクセススコープをうまく利用することで、認証ファイルを使うことなくgoogleカレンダーなどのgoogleサービスにアクセスできます。

今回は、認証ファイルを発行せずにGoogle Compute Engine（以下GCE）からgoogleカレンダーへアクセスする手順を示します。

# 手順
## Google Cloud SDKのインストール
以下のリンクを参照してください。
https://cloud.google.com/sdk/install
https://cloud.google.com/sdk/docs/initializing

## Google Calendar APIを有効にする
APIを有効にします。
Cloud SDKの場合は以下の通りです。

```sh:cmd
gcloud services enable calendar-json.googleapis.com
```

## （非公開カレンダーの場合）カレンダーにアクセスするためのサービスアカウントの準備
googleカレンダーはメールアドレスでアクセス権限を制御していますが、このメールアドレスにサービスアカウントを利用します。
公開されているカレンダーを閲覧したい場合は、この作業は不要です。

### サービスアカウントの発行
Cloud SDKの場合は以下の通りです。

```sh:cmd
gcloud iam service-accounts create ${SVCACCOUNT_NAME} \
  --display-name ${SVCACCOUNT_NAME}
```

`${SVCACCOUNT_NAME}`は適宜設定してください。

### 目的のカレンダーにアクセス権限を設定する
先に発行したサービスアカウントがアクセスできるように権限を設定します。

今回はカレンダーの情報を取得したいだけなので閲覧権限です。

サービスアカウントは`${SVCACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com` というメールアドレス形式で得られるため、アクセスしたいカレンダーの共有設定で登録します。

googleカレンダーの **[設定と共有]** -> **[特定のユーザーとの共有]** から **[ユーザーの追加]** を実行します。

<img width="508" alt="スクリーンショット 2020-02-21 19.27.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/223584/eb0b98dd-1fb9-254a-7759-12db3e4b5ba3.png">


## GCEインスタンスの設定
インスタンスは既に存在するものとします。
インスタンスを作成する段階で設定したい場合はドキュメントを参照してください。
https://cloud.google.com/sdk/gcloud/reference/compute/instances/create

### スコープの設定

**今回の罠ポイント。**

GCEインスタンスに対してgoogleカレンダーへのアクセススコープを付与する必要があります。
`compute instances set-service-account` において、 `--scope` オプションに**「インスタンスに既に付与されているスコープ」**と**「カレンダーに対するスコープ」**をカンマ区切りで記述します。

カレンダーに対するスコープは、今回についてはカレンダーへの読み取り権限があれば良いので `https://www.googleapis.com/auth/calendar.readonly` になります。
必要なスコープは以下のURLを参照してください。
https://developers.google.com/calendar/auth

`https://www.googleapis.com/auth/cloud-platform` が元々付与されていたインスタンスの場合は以下のようになります。

```sh:cmd
# インスタンスが停止しているタイミングで実行

gcloud compute instances set-service-account \
  projects/${PROJECT_ID}/zones/${ZONE}/instances/${INSTANCE_NAME} \
  --scopes https://www.googleapis.com/auth/cloud-platform,https://www.googleapis.com/auth/calendar.readonly
```

https://cloud.google.com/sdk/gcloud/reference/compute/instances/set-service-account

#### 現在のスコープの調べ方
インスタンスに既に付与されているスコープがわからない場合は、 `compute instances describe` で調べます。

```sh:cmd
gcloud compute instances describe ${INSTANCE_NAME}
```

インスタンスの情報が出力されるので、 `scopes` を探します。

```yml:output
......

serviceAccounts:
- email: xxx@yyy.gserviceaccount.com
  scopes:
  - https://www.googleapis.com/auth/devstorage.read_only
  - https://www.googleapis.com/auth/logging.write
  - https://www.googleapis.com/auth/monitoring.write
  - https://www.googleapis.com/auth/servicecontrol
  - https://www.googleapis.com/auth/service.management.readonly
  - https://www.googleapis.com/auth/trace.append
......
```


### （非公開カレンダーの場合）サービスアカウントの設定
インスタンスにサービスアカウントを紐付けます。
公開されているカレンダーを閲覧したい場合は、この作業は不要です。

```sh:cmd
# インスタンスが停止しているタイミングで実行

gcloud compute instances set-service-account \
  projects/${PROJECT_ID}/zones/${ZONE}/instances/${INSTANCE_NAME} \
  --service-account ${SVCACCOUNT_NAME}@${PROJECT_ID}.iam.gserviceaccount.com
```

https://cloud.google.com/sdk/gcloud/reference/compute/instances/set-service-account
 

## pythonスクリプトを実行
以下は、設定したGCEインスタンス上で実行します。

### ライブラリのインストール
```sh:cmd
pip3 install google-api-python-client pprint
```

### pythonスクリプトの作成
カレンダーを読み込むためのスクリプトを書いていきます。

```python:fetch_calendar_events.py
import googleapiclient.discovery
import google.auth
import datetime
import sys
from pprint import pprint

def create_credentials():
    credentials, _ = google.auth.default()
    return credentials


def create_service(credentials):
    return googleapiclient.discovery.build('calendar', 'v3', credentials=credentials)


def fetch_calendar_events(service, calendar_id):
    now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
    ret = service.events().list(calendarId=calendar_id, timeMin=now,
                                        maxResults=10, singleEvents=True,
                                        orderBy='startTime').execute()
    return ret


def main(calendar_id):
    credentials = create_credentials()
    service = create_service(credentials)
    events = fetch_calendar_events(service, calendar_id)
    pprint(events)


if __name__ == '__main__':
    args = sys.argv
    calendar_id = args[1]
    main(calendar_id)
```

### 実行
実行します。
引数にカレンダーIDを与えてあげてください。

```sh:cmd
# python3 fetch_calendar_events.py <calendar_id>

# 公開カレンダーの例
python3 fetch_calendar_events.py go1v09pqgsfc586u5ugeco97j4@group.calendar.google.com

```

上記コマンドを実行すると、[ほたてカレンダーの情報](https://calendar.google.com/calendar/embed?src=go1v09pqgsfc586u5ugeco97j4%40group.calendar.google.com&ctz=Asia%2FTokyo)が表示されます。

```sh:output
{'accessRole': 'reader',
 'defaultReminders': [],
 'description': 'ほたてちゃん親衛隊から\n'
                'ほたてちゃんを襲いたいチェイサーから\n'
                'ほたてちゃんに喰われたい女装子から\n'
                'ほたてちゃんご本人まで\n'
                '\n'
                '※非公式カレンダーです。\n'
                '※ご利用は自己責任でお願いします。\n'
                '※バグのせいで欠勤になっても責任は負いかねます。\n'
                '※バグ報告や機能改善提案は @hkak03key まで',
 ...
}
```

# 補足
## サービスアカウント
サービスアカウントとは、GCP上における仮想のユーザとみなすことができます。
AWSではIAMロールに相当するものと考えられます。
（ただし、AWSにはロールにポリシーを直接アタッチするのに対し、GCPでは「役割」に権限を定義し、サービスアカウントに対してアタッチします。）

GCPでは、実際のユーザをプロジェクトに追加し、権限を与えることができます。
一方で、何らかのシステムにおいては実際のユーザは必要ない場合がほとんどです。
このような場合に、サービスアカウントを利用します。
例えば、今回のようにサービスアカウントを紐付けることで、権限を制御することができます。

一方で、サービスアカウントは「googleサービスへのアクセスユーザ」として利用できます。
つまり、非公開のgoogleカレンダーに対して共有ユーザとしてサービスアカウントを指定すると、そのカレンダーに対してアクセスが可能になります。

## スコープ
スコープについての十分な理解は無いので、今回この記事を書くにあたって理解したことを書くと、

```sql
最終的なアクセス権限 = 
  (
    "https://www.googleapis.com/auth/cloud-platform" 
    AND
    {IAMで定義されたservice accountが持つgcpに対する権限} 
  ) 
  OR 
  {gcp以外のgoogleサービスへのスコープ}
```

となっていました。

googleカレンダーはgcp以外のgoogleサービスへのスコープなので、別途与えなければいけないようです。


# おわりに
認証ファイルを使うことなくGCEからgoogleカレンダーへアクセスする手順を示しました。

AWSのiamロールに慣れきっていると、ファイルレスで認証できたほうが幸せ感ありますよね。
ただ、ぽちぽちで設定してると見通しが悪くなるのでTerraform等でIaCをやりたくなります。

ちなみに、Google Cloud Functions（以下GCF）ではスコープという概念がないようです（2020-02現在）。
「これは詰んだ...」と思いながらダメ元でGCFで試したら**動いた**。
GCFではscope全許可のようですね...。それはそれでいいのだろうか？と思いつつ。。

それでは、**本来の目的である**「ほたてbotのpython移植ミッション」を進めます。
**GASはもう書きたくない！！！！！！**

それでは、また次回！

# 余談
スコープの件でめっちゃ詰んでたんですが、GCPUGのSlackに質問投げたら1時間で解決しました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Q. 星咲さんはどこで手間取ってたの？<br>A. GCEからカレンダーにアクセスできなくて泣いてた<br>ちなみに質問投げたら1時間で解決した模様<a href="https://t.co/rY9U9XdESJ">https://t.co/rY9U9XdESJ</a></p>&mdash; 星咲ひかる@女装子エンジニアは月収を上げて新宿に住みたい (@hkak03key) <a href="https://twitter.com/hkak03key/status/1227258238019596288?ref_src=twsrc%5Etfw">February 11, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

で、回答してくださった@apstndbさんの ~~マギレコ・オタク~~ イケメンコメントはこちら。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">運命を変えたいなら GCPUG Slack に来て<br>この Slack で GCP ユーザーは救われるから</p>&mdash; _ (@apstndb) <a href="https://twitter.com/apstndb/status/1227265755634262018?ref_src=twsrc%5Etfw">February 11, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

というわけで、みんなもGCPUG Slackに入ろう！！！！！！！
https://gcpug.slack.com/