さくらのクラウドのサーバの起動/停止をスケジューリングする

## 概要
以前同じ内容でこちらの[記事](https://qiita.com/shztki/items/7676ebbfabb089d0f296)を書いたのですが、このときは実行環境に AWS を使っていましたし、saklient というライブラリも公開が終了したようなので、作り直すことにしました。  
以下の希望がある場合は、本処理が参考になる可能性があります。

* 検証用途などのサーバを業務時間外等に停止しておくことで課金額を抑えたい

## 手法
[usacloud](https://docs.usacloud.jp/usacloud/)という CLIクライアントを利用して、対象となるサーバを特定し、起動または停止するスクリプトを作成します。  
この処理を、cron で好きな時間帯に実行することで、自動化します。  
今回は自前のサーバなどを用意しなくてもすむように、GitHub Actions を使用して、指定した時間帯で usacloud のコンテナを作成し、スクリプトを実行するようにしています。

***
**注意事項**
* スクリプトはあくまでもサンプルなので、必要に応じて修正ください。
* 全ゾーンを対象に、 `myautostartstop` タグが付いたサーバを対象としています。
* 内閣府様の[「国民の祝日」について](https://www8.cao.go.jp/chosei/shukujitsu/gaiyou.html)というサイトから、[CSV](https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv)をダウンロードして配置しておくことで、祝日を除外できるようにしてあります。ファイルに同じフォーマットで年月日を追加すれば、個別の休業日も除外可能です。
* usacloud の起動/停止コマンドでは、タグを指定して以下のように全ゾーンに対して一括実行することが可能です。ただし、すでに起動しているサーバに対して起動依頼をかけるとエラーレスポンスがあります(停止の場合は無いようです)。それが少し微妙だったので(実害があるわけでは無さそうですが)、スクリプトでは現在のステータスを確認して実行対象を限定するようにしています。停止時には `-f` (強制シャットダウン)をつけていますが、これも気になる場合は外してください。

```
usacloud server shutdown --zone all -y -f --no-wait myautostartstop
usacloud server boot --zone all -y --no-wait myautostartstop
```

***

## 基本的な流れ
1. GitHub のリポジトリを `プライベート` で作成してください。

	* パブリックで作成して公開してしまうと、ログが見えることになるので危険です。

1. 該当リポジトリ内の Settings で、Secrets内の Actions に以下 Actions secrets を設定してください。

	```Console:Name
	SAKURACLOUD_ACCESS_TOKEN
	SAKURACLOUD_ACCESS_TOKEN_SECRET
	```

1. 以下ファイルを配置してください。

	```shell:startstop.sh
	#!/bin/bash

	HOLIDAYS=`cut -d ',' -f 1 ./syukujitsu.csv | sed -e '1d'`
	#HOLIDAYS=`curl -sS https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv | awk -F',' 'NR>1 {print $1}'`
	TODAY=`TZ='Asia/Tokyo' date "+%Y/%-m/%-d"`
	TARGET_TAG="myautostartstop"
	TASK=$1
	SERVERSTATUS="up"
	POWERCOMMAND="boot"

	if [ "$TASK" = "start" ]; then
		SERVERSTATUS="down"
		POWERCOMMAND="boot"
	elif [ "$TASK" = "stop" ]; then
		SERVERSTATUS="up"
		POWERCOMMAND="shutdown -f"
	else
		echo "Usage: startstop.sh start|stop"
		exit 0
	fi

	echo ${HOLIDAYS} | grep ${TODAY} > /dev/null

	if [ ${?} -eq 0 ] ; then
		echo "today is holiday"
		exit 0
	fi

	for Zone in tk1a tk1b is1a is1b
	do
		echo "---- List Target Servers of $Zone ----"
		ServerIds=`usacloud server ls --tags $TARGET_TAG --zone $Zone --query="map(select( .InstanceStatus==\"$SERVERSTATUS\" )) | reverse | .[].ID" --query-driver=jq | xargs`

		for ServerId in $ServerIds
		do
			echo "---- $ServerId ----"
			usacloud server $POWERCOMMAND --zone $Zone -y --no-wait $ServerId
		done
	done

	echo "---- Servers ----"
	usacloud server ls --tags $TARGET_TAG --zone all
	```

	```yaml:.github/workflows/start.yml
	name: StartServer

	on:
	#  push:
	#    branches:
	#      - main
	  schedule:
	    - cron:  '55 23 * * 1-5'

	jobs:
	  node-docker:
	    runs-on: ubuntu-latest
	    container: #起動するコンテナイメージを指定
	      image: ghcr.io/sacloud/usacloud #指定のdockerイメージを使用

	    env:
	      SAKURACLOUD_ACCESS_TOKEN: ${{ secrets.SAKURACLOUD_ACCESS_TOKEN }}
	      SAKURACLOUD_ACCESS_TOKEN_SECRET: ${{ secrets.SAKURACLOUD_ACCESS_TOKEN_SECRET }}
	      SAKURACLOUD_ZONE: "is1a"
	    steps: #dockerコンテナ内でステップを実行
	      - name: Log usacloud version
	        run: |
	          usacloud -v #バージョンの確認
	          cat /etc/os-release #Linuxバージョンの確認
              apk add --update --no-cache tzdata
	      - uses: actions/checkout@v2 #次ステップでファイル読み込むのでクローンが必要
	      - name: Run a script
	        run: sh ./startstop.sh start
	```

	```yaml:.github/workflows/stop.yml
	name: StopServer

	on:
	#  push:
	#    branches:
	#      - main
	  schedule:
	    - cron:  '55 8 * * 1-5'

	jobs:
	  node-docker:
	    runs-on: ubuntu-latest
	    container: #起動するコンテナイメージを指定
	      image: ghcr.io/sacloud/usacloud #指定のdockerイメージを使用

	    env:
	      SAKURACLOUD_ACCESS_TOKEN: ${{ secrets.SAKURACLOUD_ACCESS_TOKEN }}
	      SAKURACLOUD_ACCESS_TOKEN_SECRET: ${{ secrets.SAKURACLOUD_ACCESS_TOKEN_SECRET }}
	      SAKURACLOUD_ZONE: "is1a"
	    steps: #dockerコンテナ内でステップを実行
	      - name: Log usacloud version
	        run: |
	          usacloud -v #バージョンの確認
	          cat /etc/os-release #Linuxバージョンの確認
              apk add --update --no-cache tzdata
	      - uses: actions/checkout@v2 #次ステップでファイル読み込むのでクローンが必要
	      - name: Run a script
	        run: sh ./startstop.sh stop
	```

1. `start.yml` および `stop.yml` にあるとおり、日本時間の月～金の 8:55頃に起動し、17:55頃に停止するようになっていますので、必要に応じて実行時間を変更してください。

	```
	  schedule:
	    - cron:  '55 23 * * 1-5'

	  schedule:
	    - cron:  '55 8 * * 1-5'
	```

	* 必ずしも時間通りに実行されるかはわかりませんので、気になる場合は実行タイミングを複数作るか、負荷の少なそうな時間帯を探すか、おとなしく自前の実行環境を用意した方がいいかもしれません。
	* 実行時間については公式の[情報](https://docs.github.com/ja/actions/using-workflows/events-that-trigger-workflows#schedule)も参照ください。

	```
	ノート: scheduleイベントは、GitHub Actionsのワークフローの実行による高負荷の間、遅延させられることがあります。 
	高負荷の時間帯には、毎時の開始時点が含まれます。 
	遅延の可能性を減らすために、Ⅰ時間の中の別の時間帯に実行されるようワークフローをスケジューリングしてください。
	```

## サンプルログ
起動時のログを確認すると、以下のように見えます。  
このときは 2つのサーバが対象になっていて、問題無く起動されています。  
なお停止時は、InstanceStatus が cleaning など、途中の状態になっていることもありました。  
`--no-wait` をつけてコマンドを実行しているためです。

```
Run sh ./startstop.sh start
---- List Target Servers of tk1a ----
---- List Target Servers of tk1b ----
---- ***********0 ----
---- ***********9 ----
---- List Target Servers of is1a ----
---- List Target Servers of is1b ----
---- Servers ----
+------+--------------+-----------------------------------+-------------------------+-----+--------+-----+-------------------+----------------+----------------+----------------+
| Zone |      ID      |               Name                |          Tags           | CPU | Memory | GPU |     IPAddress     | Upstream(Mbps) | InstanceStatus |  InstanceHost  |
+------+--------------+-----------------------------------+-------------------------+-----+--------+-----+-------------------+----------------+----------------+----------------+
| tk1b | ***********9 | ****************************-a... | [@auto-reboot @group... | 1   | 2      | 0   | ***.**.***.***/24 | shared/100     | up             | sac-tk1b-sv*** |
| tk1b | ***********0 | ****************************-a... | [@auto-reboot @group... | 1   | 2      | 0   | ***.**.***.***/24 | shared/100     | up             | sac-tk1b-sv*** |
+------+--------------+-----------------------------------+-------------------------+-----+--------+-----+-------------------+----------------+----------------+----------------+
```

## 最後に
管理サーバを用意して cron で実行する方が確実なのですが、そこにお金をかけたくない・・・という場合には、こういった方法もお手軽でおもしろいかなと思います。  
とはいえ、予定通りに実行されない結果、クラウドの課金が増えたら本末転倒なので、一長一短かもしれませんが。  
サーバの作成も Terraform で実行している場合は、そのコードとまとめて管理することもできて、ちょうどよいかもしれません。  
参考になれば幸いです。  
どうぞよいさくらのクラウドライフを！