Cloud Scheduler をgcloudコマンドでゆるーく触ってみる


## モチベーション

cron的なものをGCPで実現しようとすると、GAE cronかGASのトリガーを利用することが多かったのですが、Cloud SchedulerがBetaとしてリリースされました。
画面からのjob作成では細かい設定ができなさそうだったので、gcloudを利用し、細かいパラメータに関してドキュメントを把握しつつゆるく触ってみます。  
作業ログ形式なので、実際に同じようにコマンドを打っていくことでgcloudコマンド経由でschedulerのjobを3つのターゲット（pub/sub, AppEngine, HTTP）を利用して登録することができると思います。（gcloudコマンドになれている人向けで若干省いている点もあるので不親切な部分があるかもしれません）  

では進めていきましょう。

## 事前準備

- gcloudコマンドのインストールが済んでいること
- gcloudコマンドのログイン、GCPプロジェクトの作成が済んでいること
- プロジェクトのbillingが有効になっていること
- プロジェクトのcloud sheduler apiが有効になっていること
- プロジェクトのpub/sub apiが有効になっていること

## gcloud beta コマンドのインストール

gcloudコマンドでBeta機能を試すためのbetaコンポーネントをインストールします。  
すでにインストールしてある人はこの作業をskipして大丈夫です。

```
gcloud components install beta --quiet
```

念の為、コンポーネント一式をアップデートしておきましょう

```
gcloud components update --quiet
```

(--quiet オプションは y/n の入力をスキップするためのオプションなので、 y/n をどうしても押したい人は外して実行しましょう)

## ターゲットとなるpub/subのtopicを作成する

以下のコマンドでジョブのターゲットとするpub/subを作成します。  
pub/subターゲットのscheduleを登録する場合にはpub/subを事前に作成しておく必要があります。  

```
gcloud pubsub topics create cron-topic --project ${PROJECT_ID}
```

Created topic[projects/{{PROJECT_ID}}/topics/{{TOPIC_NAME}}] のように表示されればOK。  

以下のコマンドを実行し、pub/subのサブスクリプションを作成します。

```
gcloud pubsub subscriptions create cron-sub --topic cron-topic --project ${PROJECT_ID}
```

## 1. ジョブを作成する (target: pub/sub)

gcloudコマンドを利用してjobを作成してみます。
gcloudコマンドで作成する場合には、pub/sub, AppEngine http, httpの3つに対してそれぞれ異なるコマンドでの設定を行う必要があるようです。
ひとまず、先程作成したpub/subに向けて定期ジョブを実行できるようにしてみましょう。

### pub/subに対するjobを作成する

- スケジュール指定はcrontab互換指定と、従来のAppEngine Cron構文の両方を利用することができるようです(every 3 hours など)、AppEngineからの移行の際には便利ですね。
- `pub/subターゲットの場合には再試行の設定オプションが提供されない` ことに注意する必要があるようです。

実際にコマンドを実行してjobを作成してみます

```
gcloud --project ${PROJECT_ID} beta scheduler jobs create pubsub pubsub-job --schedule "0 */3 * * *" --topic cron-topic --message-body "OreOre"
```

pubsub-job という名前で3時間おきにcron-topicに対し `OreOre` というメッセージを送ります。

## pub/sub側でjobを確認する

Schedulerが実行されるとpub/subにjobが送られますので、その内容を確認してみます。

```
gcloud pubsub subscriptions pull cron-sub --limit 5 --project ${PROJECT_ID}
```

おそらくすぐに実行しても `Listed 0 items.` という感じでjobが表示されないと思います。  

## 強制的にjobを実行してみる

### 登録しているジョブを確認してみる

以下のコマンドでjobを確認してみます

```
gcloud beta scheduler jobs list --project ${PROJECT_ID}
```

おそらく以下のように表示されるはず

```
ID          LOCATION         SCHEDULE (TZ)          TARGET_TYPE  STATE
pubsub-job  asia-northeast1  0 */3 * * * (Etc/UTC)  Pub/Sub      ENABLED
```

### 強制的にjobを実行する

```
gcloud beta scheduler jobs run pubsub-job --project ${PROJECT_ID}
```

### もう一度pub/subを確認してみる

```
gcloud pubsub subscriptions pull cron-sub --limit 5 --project ${PROJECT_ID}
```

今度は1件表示されるはずです。

### 確認ができたので、pub/subのjobを消しておく

データをpullしつつ、ackを返して消します。

```
gcloud pubsub subscriptions pull --auto-ack cron-sub --project ${PROJECT_ID}
```

## 一旦整理(1)

ここまでで、  

- pub/subに定期的にjobを投入する（pub/sub経由での定期実行）ことができた
- 登録したjobをコマンドラインから確認できた

という感じですが、先程のjobを確認した際にTZが（Etc/UTC）となっていたのが気になりますね。  
timezoneをAsia/Tokyoとして再度登録してみましょう。

## TimeZone指定してjobを投入する

### 先程登録したジョブを削除する

timezoneを変更したいので登録済みのjobを削除します。

```
gcloud beta scheduler jobs delete pubsub-job --project ${PROJECT_ID}
```

### TimeZone指定し、再度jobを登録する（AppEngine Cron）

timezoneとして `Asia/Tokyo` をパラメータとして渡します。  
さらに、毎分実行して見ようと思いますので、 `every 1 mins` という感じにAppEngine cron構文で設定してみます。

```
gcloud --project ${PROJECT_ID} beta scheduler jobs create pubsub pubsub-job --schedule "every 1 mins" --topic cron-topic --message-body "OreOre" --time-zone "Asia/Tokyo"
```

### 投入されたjobを確認する

以下のコマンドでtimezoneを指定して登録したjobを確認します。

```
gcloud beta scheduler jobs list --project ${PROJECT_ID}
```

以下のような表示になり、今度はtimezoneも正しく設定されていそうです

```
ID          LOCATION         SCHEDULE (TZ)           TARGET_TYPE  STATE
pubsub-job  asia-northeast1  every 1 mins (Asia/Tokyo)  Pub/Sub      ENABLED
```

### pub/subに投入されたjobを確認する

数分時間をおいて以下のコマンドを実行すると複数のjobがpub/sub内に投入されていることが確認できると思います。

```
gcloud pubsub subscriptions pull cron-sub --limit 5 --project ${PROJECT_ID}
```

### 後処理

放って置くとpub/subに大量のjobが投入されてしまうので、jobを停止(削除)します。
（停止はおそらくなさそうです）

```
gcloud beta scheduler jobs delete pubsub-job --project ${PROJECT_ID}
```

pub/sub内のjobも削除しておきましょう(場合によっては何度か実行し、jobが空になるまで実行しましょう)

```
gcloud pubsub subscriptions pull cron-sub --limit 100 --auto-ack --project ${PROJECT_ID}
```

## 一旦整理(2)

ここまでで、

- Appengine Cron形式でjobを投入することができた
- TimeZoneを `Asia/Tokyo` として投入することができた

謎) documentには `--max-backoff` `--max-doublings` などの設定があるが、こちらはpub/subへの投入に失敗した場合のbackoff設定なのかな？
```
Notice that Pub/Sub target configuration does not provide options for setting retries, because Pub/Sub provides its own retry mechanism and policy.
```

## 2. ジョブを作成する (target: AppEngine)

pub/subのjob作成はできたので、AppEngineのjob作成を試してみます。
前提としてすでにAppEngine上に何かしらのアプリケーションをデプロイしている想定で進めます。  

AppEngineのエンドポイントはapp.yamlの `login:admin` を利用して保護することができるようです。
`login:admin` を利用することで外部からのリクエストを防ぎつつ、Schedulerからはアクセスさせるということが実現できます。  

AppEngineの場合には、HTTPメソッドを指定できます。デフォルトはPOSTです。
また、サービス、バージョン、インスタンスの指定もできるようです。
送信するデータはPOST,PUTのrequest bodyとして送られます。
殆どの場合にはデフォルトて十分ですが、リトライ方法を指定できます。

では、jobを作成してみます

```
gcloud beta scheduler jobs create app-engine appengine-job --schedule "every 1 mins" --time-zone "Asia/Tokyo" --relative-url "/" --message-body "Oredayo" --project ${PROJECT_ID}
```

### 投入したjobを確認してみる

pub/subと同様で以下のコマンドで確認してみます。

```
gcloud beta scheduler jobs list --project ${PROJECT_ID}
```

以下のような表示がされるはずです

```
ID             LOCATION         SCHEDULE (TZ)              TARGET_TYPE  STATE
appengine-job  asia-northeast1  every 1 mins (Asia/Tokyo)  App Engine   ENABLED
```

### HTTPメソッドを変更してみる

先程書いたようにSchedulerからはデフォルトでPOSTが送られます。せっかくなのでGETも試してみましょう

```
gcloud beta scheduler jobs create app-engine appengine-job --schedule "every 1 mins" --time-zone "Asia/Tokyo" --relative-url "/" --http-method "GET" --project ${PROJECT_ID}
```

GETの注意点は `--message-body` オプションを利用できません。
Targetにservice, version などを指定したい場合には、 `--servce` , `--version` オプションが利用できます。

もちろん、変更（作成）する前に以下のコマンドで同名のjobを消しておくことをお忘れなく、

```
gcloud beta scheduler jobs delete appengine-job --project ${PROJECT_ID}
```

AppEngineへのログを確認した際に、POSTで出力されていたログがGETに変わればOKです。

## 一旦整理(3)

ここまでで、

- AppEngine TargetへのSchedule jobを登録することができた。
- POST/GET jobを登録することができた

上で試してはいませんが、リトライ系のオプションはパラメータ的にAppEngineのTQ時のリトライと同じようなイメージではないかと。
別の機会で検証してみようと思います。

## 3. ジョブを作成する (target: HTTP)

HTTPターゲットのjobを作成してみます。
HTTP/HTTPSの任意のターゲットに対して定期実行ができます。もちろんターゲットは外部に公開されている必要があります。

注意事項などに関してはAppEngineなどとほぼ同じです。（URLがFQDNである必要があるぐらい）  

では、以下のコマンドでjobを登録してみましょう。

```
gcloud beta scheduler jobs create http http-job --schedule "every 1 mins" --uri ${TARGET_URL} --http-method GET --time-zone "Asia/Tokyo" --project ${PROJECT_ID}
```

### 投入したjobを確認する

```
gcloud beta scheduler jobs list --project ${PROJECT_ID}
```

以下のような表示が確認できるはずです

```
ID        LOCATION         SCHEDULE (TZ)              TARGET_TYPE  STATE
http-job  asia-northeast1  every 1 mins (Asia/Tokyo)  HTTP         ENABLED
```

URI指定したターゲット側のログから定期実行を確認することができると思います。

### 確認が終わったらjobを消しておきましょう

以下のコマンドで削除が行なえます。

```
gcloud beta scheduler jobs delete http-job --project ${PROJECT_ID}
```

## まとめ

- 一通りgcloudコマンド経由で触ってみましたが、ドキュメントに注意点なども書かれているのでとても良い
- AppEngine cron記法が使えるので、AppEngine cronからの移行は比較的容易にできそう。
- リトライはある程度デフォルト値が設定されているので、失敗するようなターゲットでも再実行はされる(?)
  - `--max-retry-attempts` などがdefault 0なので無制限？
- HTTPターゲットの際のURLはエンコード後の長さで2083 characters。

## オッと思った点

- pub/sub ターゲットの際のリトライオプションがドキュメントには書いてあるが、 `pub/subターゲットの場合には再試行の設定オプションが提供されない` とも書いてあるのでよくわからなかった。
- HTTPターゲットの場合、ターゲットのURLはドメイン認証しなくても普通に登録できた。
- Cloud Schedulerは同時に2つの未処理の実行を許可しない。n+1は実行nが終了するまで開始することはない。（cronと違うので注意）


## 参考

https://cloud.google.com/scheduler/docs/quickstart?hl=en
https://cloud.google.com/sdk/gcloud/reference/beta/scheduler/jobs/create/pubsub?hl=ja