GitHubActionsのSelf-hosted runnerを必要な時だけ起動する


以前書いた「GitHubActionsのSelf-hosted runnerで、SpringNativeのビルド時間を短縮する」の続きです。

SpringNativeでNativeイメージ化する際のビルド時間の短縮に「Self-hosted runner」に強めのEC2インスタンスを使用してビルド時間の短縮した話を書きました。
https://qiita.com/renave/items/561904b2988ebb6f0534

今回は費用の節約のために「Self-hosted runnerを必要な時だけ起動する」ようにしたお話です。

こちら、強めのEC2インスタンスなので起動しっぱなしにすると結構お金がかかります。
使うたびに手動で起動/停止するのも大変です。

そこで、
1：GHA実行時に標準のUbuntuのrunnerを使用し、Self-hosted runnerインスタンスを起動。
2：Self-hosted runnerでSpringNativeのビルド処理を実行。
3：ワークフローが終了したらインスタンスを停止する。
4：念のため、業務終了後の夜間にも停止をスケジュール。
するようにしました。



# 「actions-runner/run.sh」自動起動設定

インスタンスを起動する際に、自動で「Self-hosted runner」の「actions-runner/run.sh」を実行させる必要があります。

これには「rc.local」を利用します。

rootでrc.local作成

```
sudo su -
sudo vi /etc/rc.local
```

既にインストール済みの「actions-runner/run.sh」を絶対パスで記述
rc-local.serviceで実行する場合は、rc.localファイルに「RUNNER_ALLOW_RUNASROOT="1"」を設定するのがポイントです。
そうしないとrc-local.serviceで実行できません。

```
#!/bin/bash
export RUNNER_ALLOW_RUNASROOT="1"
/home/****/actions-runner/run.sh &
```

実行権限を与えましょう
```
chmod +x /etc/rc.local
```

続いて、rc-local.serviceの自動起動設定、サービスリスタート、ステータス確認をします。

```
systemctl enable rc-local.service
systemctl restart rc-local.service
systemctl status rc-local.service
```

rc-local.serviceが以下のように正常起動していればOKです。

```
● rc-local.service - /etc/rc.local Compatibility
     Loaded: loaded (/etc/systemd/system/rc-local.service; enabled; vendor preset: enabled)
    Drop-In: /usr/lib/systemd/system/rc-local.service.d
             └─debian.conf
     Active: active (running) since Mon 2021-11-22 15:32:43 UTC; 12s ago
    Process: 1643 ExecStart=/etc/rc.local start (code=exited, status=0/SUCCESS)
   Main PID: 1644 (run.sh)
      Tasks: 15 (limit: 37282)
     Memory: 35.0M
     CGroup: /system.slice/rc-local.service
             ├─1644 /bin/bash /home/***/actions-runner/run.sh
             └─1648 /home/***/actions-runner/bin/Runner.Listener run

Nov 22 15:32:43 ********* systemd[1]: Starting /etc/rc.local Compatibility...
Nov 22 15:32:43 ********* systemd[1]: Started /etc/rc.local Compatibility.
Nov 22 15:32:45 ********* rc.local[1648]: √ Connected to GitHub
Nov 22 15:32:46 ********* rc.local[1648]: Current runner version: '2.284.0'
Nov 22 15:32:46 ********* rc.local[1648]: 2021-11-22 15:32:46Z: Listening for Jobs
```


# Self-hosted runnerのEC2インスタンスを停止→起動してみましょう

### EC2コンソールから停止
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27112/2e396d10-f461-e88b-6072-dd14d9ee4d1b.png)

### Githubでrunnerの確認
停止したので「Offline」になりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27112/0d6d62df-5745-b77e-1d87-2d229181ab58.png)


### GithubActionsのworkflow
こんな感じの起動スクリプトをworkflowに書いて動かしてみましょう。

```
name: Self-hosted runner startup workflow

　～中略

env:
  SHR_INSTANCE_ID: '<YOUR_SELF_HOSTED_RUNNER_INSTANCE_ID>'

jobs:
  startup:
    name: startup
    runs-on: ubuntu-20.04
    timeout-minutes: 10

　～中略

    steps:
      #AWS Credentials
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{secrets[matrix.cfg.access-key-id]}}
          aws-secret-access-key: ${{secrets[matrix.cfg.secret-access-key]}}
          aws-region: ${{secrets[matrix.cfg.default-region]}}

      - name: startup self-hosted runner
        run: |
          SHR_INSTANCE_STATUS=$(aws ec2 describe-instance-status --instance-ids ${SHR_INSTANCE_ID} | jq -r .InstanceStatuses[].InstanceStatus.Status)
          echo "SHR_INSTANCE_STATUS=${SHR_INSTANCE_STATUS}"

          if [ -z "${SHR_INSTANCE_STATUS}" ] ; then
            echo "Starting Self-Hosted runner. Please wait."
            aws ec2 start-instances --instance-ids ${SHR_INSTANCE_ID}
            sleep 10
            for ((i=1; i<100; i++)); do
              SHR_WAIT_STATUS=$(aws ec2 describe-instance-status --instance-ids ${SHR_INSTANCE_ID} | jq -r .InstanceStatuses[].InstanceStatus.Status)
              echo "[Check status...${i}] ${SHR_WAIT_STATUS}"
              if [ "${SHR_WAIT_STATUS}" = "ok" ] ; then
                echo "Self-Hosted runner started."
                break;
              fi 
              sleep 10
            done
          else
            echo "Self-Hosted runner already is running."
          fi

          exit 0

```
※ forで回している理由は、whileだとエラーになってしまうみたい。GHAでは禁止されているのかな？


こんな感じで起動処理が走ります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27112/0584b1df-35bd-f8c9-9365-2a3ee939c218.png)

起動済みであればこんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27112/179bcf48-bab5-adc0-52c9-f9e90ab3cd78.png)





### 起動後
actions-runnerのスクリプトが起動すると、Idle状態になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27112/b2f23eec-8625-f2b9-ef17-eecc90d21000.png)



### 後続のビルド処理は「runs-on: self-hosted」で「needs: [startup]」のworkflowを作ればOK

```
  build:
    name: build
    needs: [startup]
    runs-on: self-hosted

　以下、省略～
```



# ジョブ終了後に「Self-hosted runner」を停止する

こちらもbuild後に実行させるように「shutdown」ワークフローを書きます。

```
  shutdown:
    name: shutdown
    needs: [build]
    runs-on: ubuntu-20.04
    timeout-minutes: 10

　～中略

    steps:
      #AWS Credentials
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{secrets[matrix.cfg.access-key-id]}}
          aws-secret-access-key: ${{secrets[matrix.cfg.secret-access-key]}}
          aws-region: ${{secrets[matrix.cfg.default-region]}}

      - name: shutdown self-hosted runner
        run: |
          SHR_WAIT_STATUS=$(aws ec2 describe-instance-status --instance-ids ${SHR_INSTANCE_ID} | jq -r .InstanceStatuses[].InstanceStatus.Status)
          echo "SHR_INSTANCE_STATUS=${SHR_INSTANCE_STATUS}"
          if [ "${SHR_WAIT_STATUS}" = "ok" ] ; then
            aws ec2 stop-instances --instance-ids ${SHR_INSTANCE_ID}
            echo "Self-Hosted runner is stopping."
            sleep 10
            for ((i=1; i<100; i++)); do
              SHR_WAIT_STATUS=$(aws ec2 describe-instance-status --instance-ids ${SHR_INSTANCE_ID} | jq -r .InstanceStatuses[].InstanceStatus.Status)
              echo "[Check status...${i}] ${SHR_WAIT_STATUS}"
              if [ -z "${SHR_WAIT_STATUS}" ] ; then
                echo "Self-Hosted runner stopped."
                break;
              fi 
              sleep 10
            done
            echo "Self-Hosted runner stopped."
          else
            echo "Self-Hosted runner already stopped."
          fi 
          
          exit 0
```

こんな感じで、停止処理がビルド処理完了後に動きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27112/bbcc7d56-00ab-2a99-bed4-d1824e865cf2.png)


停止しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27112/eef5c745-7eed-25a4-3349-978d6803e6af.png)




# 業務終了時間に停止させる

停止忘れが無いように、念のため業務に影響のない夜間に自動で停止をスケジュールしておきます。

自動停止には、AWSのEvent Bridgeで「Stop-Instances」のイベントを使用します。
こんな感じで設定できます。

16:00 UTC（JSTで25時）に停止するようにしてます。
この辺は運用に合わせて適宜設定するといいと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27112/a6e345a1-96a4-4c65-20c8-995e8063157f.png)

公式ドキュメントも参考にしてください
https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-create-rule-schedule.html



# 今後の改善
「停止処理」は、ビルド処理がひととおり完了した後に実施していますが、複数人で同時にworkflowを走らせた場合などに困ることがありそうなので、この点はランナーの状態を確認したり、複数台用意して、offlineのランナーを選出して使ったり。
不都合があれば、そんな処理を入れていこうかなと思っています。



# P.S.
いかがでしょうか？
Self-hosted runnerにハイスペックなマシンを使うことでCIの処理時間（今回はSpringNativeのビルド）を短くすることができますが、費用も気にした方がいいのでこういう形にしてみました。

今回の処理を入れることでビルド時間はインスタンス起動時間分遅くなりますが、お金のことなのでそこはどちらを取るか。といったところでしょうか。
なお、EC2インスタンスを停止していてもEBSボリューム代はかかるので、ご留意ください。

参考になれば幸いです。