Goでcrontabのようなツールを作ってデーモン化してみた

## crontabにGoを動かす処理を登録してみたが動かず
普段業務ではタスクスケジューラにcrontabを使用している。そのためGoでバッチ処理を管理するときも当然のようにcrontabで管理するものだと思っていたが、なぜかcrontabが動作せず、エラーログも残らないという現象に直面。

調べてみても、Goをcrontabで実行しようとする人がおらず、代わりにGoのタスクスケジュールライブラリの紹介記事をいくつか発見したため、こちらで実装してみることにした。

## gocronを使用してタスクスケジューリングを行う
Goのタスクスケジュールライブラリはいくつか存在するようだが、
こちらで無限ループなどにして待ち状態を実装する必要がなさそうだったため、今回はgocronを採用。

Goのバージョンは1.17.6

```main.go
package main

import (
    "fmt"
	"path/filepath"

	//"github.com/jasonlvhit/gocron"
    "github.com/go-co-op/gocron"
	"github.com/joho/godotenv"
)

func funcA() {
    fmt.Println("A")
}
func funcB() {
    fmt.Println("B")
}
func funcC() {
    fmt.Println("C")
}

func main() {
	err := godotenv.Load(filepath.Join("path", "to", "dotenv", ".env")) 
	if err != nil {
		panic("can't get .env")
	}

	// gocron.Every(1).Day().At("00:40").Do(funcA) 
	// gocron.Every(1).Saturday().At("11:15").Do(funcB)
	// gocron.Every(30).Minutes().Do(funcC)
	// <-gocron.Start()

    scheduler := gocron.NewScheduler(time.Local) 
	scheduler.Every(1).Day().At("00:40").Do(funcA) // 1日1回0時40分に
	scheduler.Every(2).Saturday().At("11:15").Do(funcB)// 2週間に1回土曜日の11:15に
	scheduler.Every(30).Minutes().Do(funcC) // 30分に1回
	scheduler.StartBlocking()
}
```

冒頭のgodotenvについては後述。このサンプルコードの時点では不要。


~~gocronで始まる箇所でタスクを登録する。~~
~~Everyの中の数字はintervalとして管理されており、Dayであればinterval * Hour * 24と計算される。~~
~~つまりgocron.Every(2).Days().~ と記述すれば2日に1回処理を実行できる。~~


:::note info
~~ただし、後ろに続く関数では内部でintervalが1であることが前提の
チェックが行われているため~~
~~intervalに2以上を使用する場合は、それぞれ対応している複数形の関数を使用する必要がある~~
~~(例)~~
~~Day()-> Days()~~
~~Minute() -> Minutes()~~

github.com/jasonlvhit/gocronパッケージはforkされ
github.com/go-co-op/gocronパッケージとしてメンテナンスされているらしい。
こちらは、以前の名残でDay()とDays()などは両方ありますが、どちらも同じ動きをする。
:::


~~最後のgocron.Start()をchannel送信することで内部でboolの値を送信し続けてくれるため
これだけでタスクスケジューラの実装が完了してしまう。~~
github.com/go-co-op/gocronパッケージでは、タスクの登録は全てScheduler型が担っていて、channnel送信も関数で内部化された模様。

これから使うならこちらを推奨。[github.com/go-co-op/gocron](https://github.com/go-co-op/gocron)

## 作成したタスクスケジューラをデーモン化
サンプルコードを実行するとわかると思うが、この実装は常にプログラムを実行しておかなければならない。
そこで、バイナリにコンパイルしたものを使いデーモン化を試みる。

まずはバイナリを作成する。

```
go build -o go_cron main.go
```

次にデーモン化だが、今回はツールを使用せずCentOSのsystemdにserviceを追加する。

``` 
cd /etc/systemd/system
vi go_cron.service
```
```go_cron.service
[Unit]
Description=golang cron tool
ConditionPathExists=/path/to/go_cron


[Service]
User=root
Type=simple
ExecStart=/path/to/go_cron/go_cron
Restart=on-failure
TimeoutStartSec=300

[Install]
WantedBy=/path/to/wantedby
```
ExecStartに先ほど作成したバイナリを設定する。

個人的にハマった箇所としては以下
- Restart=on-failureをRestart=noに設定してしまうとpanicなどでgoがクラッシュしたときにサービスごと死んでしまった。on-failureにすることでサービスは立ち上がったままでいてくれる。
- WantedByを設定しないとstaticなサービスとなる。これを設定することでsystemctl enableをすることができるようになる。


.serviceファイルを書いたら、systemdに変更を反映させる
```
systemctl daemon-reload
```


サービス立ち上げ
```
systemctl start go_cron.service

# ステータス確認
systemctl status go_cron.service

go_cron.service - golang cron tool
   Loaded: loaded (/etc/systemd/system/go_cron.service; disabled; vendor preset: disabled)
   Active: active (running) since 火 2022-03-01 13:15:56 JST; 2h 27min ago
```

## Goのソースコードで環境変数を使う箇所がある場合
いざサービスが立ち上がった！とジョブが開始されるのを待っていたが動かない。
こういう時はjournalctlというコマンドでログが見れるらしい。
```
sudo journalctl | grep go_cron
```
ログを見るとGoの処理が内部でpanicを起こしていた。
もしやと思い環境変数を使う箇所を出力してみると、案の定空欄が出力された。

そこでgodotenvパッケージを利用して.envファイルの環境変数を事前に取り込んだところ、無事に動作した。冒頭のmain.goのソースの最初の3行はそういった経緯である。]

## まとめ
普段Dockerで曖昧にgoを動かしているので、buildしてバイナリを作って運用するといつもと違うケースで悩むことがあって勉強になった。
次はデーモン化ツールを使ってみようと思う。

~~あと、Saturday()などの曜日指定は週に1回のジョブ呼び出しにしか対応していないようなのでissue提案して2週間に1回土曜日とかもできるようにとかも考えてみようかな・・・~~
issue投げようとしたらリポジトリごと移行していたことが発覚したのでその旨を記事の前半に書き加えました。

## 参考記事
- [godotenvを使ったGolangでの環境変数の設定方法](https://tomokazu-kozuma.com/how-to-set-environment-variables-in-golang-using-godotenv/)
- [【Golang】godotenv を使ってenvファイルを扱う](https://www.st-hakky-blog.com/entry/2020/06/11/100000)
- [systemdのログ確認方法](https://qiita.com/marumen/items/e4c75a2617cb5d0113ce)
- [Goでスケーラブルなスケジューラを書いた](https://qiita.com/kawasin73/items/7af6766c7898a656b1ee)
- [Goでつくるdaemon処理レシピ集](https://qiita.com/mtskhs/items/ebdfecd802507404d436)