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


概要

以前同じ内容でこちらの記事を書いたのですが、このときは実行環境に AWS を使っていましたし、saklient というライブラリも公開が終了したようなので、作り直すことにしました。
以下の希望がある場合は、本処理が参考になる可能性があります。

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

手法

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


注意事項

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

基本的な流れ

  1. GitHub のリポジトリを プライベート で作成してください。

    • パブリックで作成して公開してしまうと、ログが見えることになるので危険です。
  2. 該当リポジトリ内の Settings で、Secrets内の Actions に以下 Actions secrets を設定してください。

    Name
    SAKURACLOUD_ACCESS_TOKEN
    SAKURACLOUD_ACCESS_TOKEN_SECRET
    
  3. 以下ファイルを配置してください。

    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
    
    .github/workflows/start.yml
    name: StartServer
    
    on:
    #  push:
    #    branches:
    #      - main
      schedule:
        - cron:  '15 1 * * 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
    
    .github/workflows/stop.yml
    name: StopServer
    
    on:
    #  push:
    #    branches:
    #      - main
      schedule:
        - cron:  '15 9 * * 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
    
  4. start.yml および stop.yml にあるとおり、日本時間の月~金の 10:15頃に起動し、18:15頃に停止するようになっていますので、必要に応じて実行時間を変更してください。

      schedule:
        - cron:  '15 1 * * 1-5'
    
      schedule:
        - cron:  '15 9 * * 1-5'
    
    • 必ずしも時間通りに実行されるかはわかりませんので、気になる場合は実行タイミングを複数作るか、負荷の少なそうな時間帯を探すか、おとなしく自前の実行環境を用意した方がいいかもしれません。
    • 実行時間については公式の情報も参照ください。
    ノート: 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 で実行している場合は、そのコードとまとめて管理することもできて、ちょうどよいかもしれません。
参考になれば幸いです。
どうぞよいさくらのクラウドライフを!