定期スクリプト実行環境としてのGitLab CIのススメ

[ラクスAdvent Calendar 2021](https://qiita.com/advent-calendar/2021/rakus) の2日目は、スプリットキーボードならぬデュアルキーボード歴2ヶ月の [@oohira](https://twitter.com/oohira) が、左手に Magic Keyboard、右手に REALFORCE というごった煮環境からお送りします。

昨日は「初日取ったった！」してくれた @Y-Kanoh さんの「[GASでカユいところに手が届いたTips3選](https://qiita.com/Y-Kanoh/items/4d7505bcfafebd34d197)」でした。勢い、大事ですね。

## はじめに

これまで定期実行したいちょっとしたスクリプトは、個人PCやプロジェクト共用サーバー、Jenkins などを使って実行していましたが、ここ数年で GitLab CI を使って実行する方法に完全に落ち着きました。この記事では、社内のメンバーも読者に想定しつつ、GitLab CI のよさと簡単な始め方を紹介したいと思います。

## 背景と前提

GitLab CI（や GitHub Actions）が登場する前の世界では、Linux の cron や、Windows のタスクスケジューラー、Jenkins などを使ってスクリプトを定期実行することが多かったのではないかと思います。いずれの方法でも目的は果たせますが、次のような課題もありました。

* 実行マシンを用意するのが面倒
    * 個人PCなのか共用サーバーなのかは別として、スクリプトを実行するためのマシンを用意し、デプロイする仕組みを考えなければならない
* 実行マシンのミドルウェアを管理するのが面倒
    * スクリプトで使用するプログラミング言語を実行できる環境を整備したり、実行マシン自体のOSやミドルウェアを定期的にアップデートしなければならない
* 引き継ぎするのが面倒
    * 別の人にスクリプトを引き継いでメンテしてもらう必要が出たときに、実行マシンの構成や手順などを文書化して伝えなければならない

GitLab CI を使うと、これらの問題を解決したスクリプト実行環境を簡単に構築することができます。GitHub Actions でも同じことが実現できると思いますが、弊社ではソースコード管理システムとして GitLab を採用しているため、ここでは GitLab CI を前提として話を進めます。執筆時点で確認したバージョンは次の通りです。

* GitLab v14.0.5
* GitLab v14.6.0-pre


## はじめの一歩

まずは、GitLab CI でスクリプトが実行できるミニマムの環境を作ります。

1. GitLab プロジェクトを新規作成する
1. GitLab Runner を登録する（GitLab.comの場合は不要）
    * 自社でサーバーを立てている＆Shared runner が有効になっていない場合は、 `Settings > CI/CD > Runners` から、GitLab CI を実行するためのサーバーを登録する必要があります。
        * ~~弊社メンバーの皆さまは、↓にある個別 Runner を使ってもらってOKですよ。~~ これ、一部の人にしか見えていないっぽいことが発覚してしまいました [^1]
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/71ba40d0-ab35-9ce0-43b3-2be2eda524bd.png)
1. `.gitlab-ci.yml` を作成する
    * プロジェクトルートにビルド設定を記載したファイルを作成します。リポジトリをチェックアウトしなくても、GitLab の Web 画面から作成・編集するとお手軽です

    ```yml:.gitlab-ci.yml
    image: alpine
    
    awesome-job:
      stage: build
      script:
        - echo "Hello, World!"
    ```
1. `.gitlab-ci.yml` をコミット＆push する
    * 修正をコミットすると、自動的に GitLab CI によるビルドが始まります。ビルドログなどを確認すると、`awesome-job` という名前のジョブが実行されて成功したことや、Alpine Linux をベースイメージとするコンテナ内で `script` に指定したコマンドが実行されたことなどが分かります
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/2898577c-0379-f3d6-c12a-99160ec0d463.png)
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/b757c802-80a1-cdbe-e0d7-5c67eb481766.png)

[^1]: プロジェクトロックしていない Specific runner を社内メンバーに自由に使ってもらって、 GitLab CI を手軽に試してもらおうというのがそもそも今回の記事の目的だったのに、ここにきてまさかとはこのこと。やはり管理者にお試し用の Shared runner を用意してもらうしかないか。とり急ぎ Specific runner の候補が1つも出ない場合は、`Show Runner installation instructions` の手順に従って… :pray:（ry

簡単ですね

>**POINT: 実行環境の準備が楽**
>GitLab CI は、GitLab に統合されているため、実行マシンを別に用意[^2]したり、スクリプトを実行サーバーにデプロイしたりする手間がありません。.gitlab-ci.yml に実行したいスクリプトを書くだけで、あとは GitLab CI がいい感じに実行してくれます。

[^2]: 厳密には、GitLab CI の実行に使う GitLab Runner を誰かが用意する必要があります。そのため、GitLab CI を使ったからといって完全に実行マシンの管理が不要になるわけではありませんが、GitLab Runner さえ用意すれば他の大多数の人はその恩恵を受けるだけで済みます。


## ミドルウェアを変更する

ここで、もう少し本格的なスクリプトを作りたくなったとしましょう。シェルスクリプトでは心もとないので、Node.js に切り替えるとします（Go でも Ruby でも好きな言語をどうぞ）。

1. `.gitlab-ci.yml` を変更する
    * Node.js がセットアップ済みの軽量イメージとして [node:14-alpine](https://hub.docker.com/_/node) を使ってみます [^3]

    ```yml:.gitlab-ci.yml
    image: node:14-alpine
    
    awesome-job:
      stage: build
      script:
        - npm xmas
    ```

1. 実行ログを確認する :christmas_tree: 
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/e6138339-7038-d3d3-75ff-04e8f604c831.png)


>**POINT: ミドルウェアの管理や更新が楽**
>GitLab CI は、コンテナベースの実行環境なので、スクリプトを実行するOSやプログラミング言語を変更しても、新たにミドルウェアをセットアップする手間がありません。.gitlab-ci.yml に必要なバージョンのミドルウェアを指定するだけで準備が完了します。

[^3]: `npm xmas` がとうとう削除されたっぽいので、意図的に少し古めの Node.js を使用しています。


## 定期実行する

このままでは（CIの本来の期待通りではありますが）コミットするたびに実行されてしまうので、スケジュール登録して定期実行できるようにします。

1. スケジュール実行のときだけビルドするよう `.gitlab-ci.yml` を変更する

    ```yml:.gitlab-ci.yml
    image: alpine
    
    awesome-job:
      stage: build
      rules:
        - if: $CI_PIPELINE_SOURCE == "schedule"
      script:
        - echo "Hello, World!"
    ```

1. スケジュール設定を登録する
    * `CI/CD > Schedules > New schedule` からスケジュール設定を登録します。cron と同じ書式で実行間隔を指定できます
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/8524bac4-13ed-6f5d-b0e7-d5da0aeb7ef7.png)
    * 登録が完了すると、次回実行予定日時が確認できます。オンプレの場合は、[GitLab の設定によって実行間隔が期待通りにならないことがある](https://docs.gitlab.com/ee/ci/pipelines/schedules.html#advanced-configuration) ので注意してください
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/0558482a-9448-cdb1-996a-a046fdc48461.png)

1. スケジュール設定した日時に自動実行されるのを待つ
1. 手動で即時実行する
    * 動作確認などで次回スケジュール実行まで待てない場合は、`Play` ボタンから手動でキックできます
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/13860d09-2692-634a-ac08-195858410b9a.png)


## 外部システムと連携する

スクリプトを実行する仕組みは整いましたが、このままでは GitLab CI のログでしか出力を確認することができません。多くの場合は、Slack や Mattermost のようなチャットツールに結果を投稿したり、Git にコードをコミットしたり、別のシステムとの連携が期待されます。

ここでは、Mattermost に投稿する bot を作ってみます。

1. Webhook URL を取得する
    * Mattermost 側で [Incoming Webhook](https://docs.mattermost.com/developer/webhooks-incoming.html) を実行するためのURLを発行します
1. `.gitlab-ci.yml` を修正する
    * Mattermost にメッセージを投稿するようスクリプトを変更します。curl コマンドを追加でインストールしている点や、URLをハードコーディングせずに環境変数としている点に注意してください（説明の都合上、インラインでコマンドを書いていますが、エスケープがつらいので実際は別ファイルに切り出すとよいでしょう）

    ```yml:.gitlab-ci.yml
    image: alpine
    
    awesome-job:
      stage: build
      rules:
        - if: $CI_PIPELINE_SOURCE == "schedule"
      script:
        - apk add curl
        - 'curl -k -X POST -H "Content-Type: application/json"
          -d "{\"icon_emoji\": \"santa\", \"username\": \"santa_bot\", \"text\": \"Hello, Mattermost. :tada:\"}"
          "${MATTERMOST_URL}"'
    ```

1. スケジュール設定に環境変数を登録する
    * `CI/CD > Schedules` からスケジュール設定を開き、スクリプトに渡したい環境変数の値を設定します。スクリプト中にハードコーディングしないことで、デバッグ中に接続先を変更するなど融通がききやすくなります
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/34a52d07-5ff7-5b15-6b7a-3d363249c4b6.png)
    * なお、ここで指定する環境変数の値は、ビルドログ中にも展開されません
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/cc8281d9-8613-03bd-c3f4-b1f53f52b2e9.png)
1. 定期実行されるのを待つ or 手動実行する
    * GitLab CI で実行されたスクリプトから Mattermost への投稿 [^4] が成功します :tada:
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/71f2f65c-923f-d5a0-0cd9-bdf5929491ad.png)

[^4]: 当たり前と言えば当たり前ですが、GiLab CI が実行するコンテナから Mattermost へアクセスできる必要があります。GitLab.com を使っているけど Mattermost は社内ネットワークで稼働している、と言った場合は面倒かもしれません

好きな言語で書いたスクリプトからチャットへ投稿ができるようになったので、あとはスクリプトで何を投稿するか、アイデア次第です。

>**POINT: 引き継ぎが楽**
>スクリプトだけでなく、ミドルウェアまで含めた前提条件が .gitlab-ci.yml にコード化されるため、スクリプトの実行に何が必要かを把握しやすくなります。また、スケジュール設定や環境変数の値も含めてすべて GitLab 上に一元化されるため、引き継ぎはリポジトリを伝えれば何とかなります（多分）。


## 成果物を保存する

スクリプトの出力を二次利用する別の方法として、生成したファイルを GitLab に保存しておき、後から参照できるようにすることもできます。

1. `.gitlab-ci.yml` を修正する
    * 外部サービスから取得したネコ画像を `artifacts` として保存するよう指定します

    ```yml:.gitlab-ci.yml
    image: alpine
    
    awesome-job:
      stage: build
      rules:
        - if: $CI_PIPELINE_SOURCE == "schedule"
      script:
        - apk add curl
        # Powered by Cat as a service (CATAAS)
        - curl -s "https://cataas.com/cat/gif" -o cat.gif
      artifacts:
        paths:
          - cat.gif
    ```
1. 実行ログの画面から確認する
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/55a78f5d-1968-cb3f-918d-a8fd90629d9c.png)
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33668/24da42f9-2f96-de4b-b34d-3f852e15cd25.png)

:cat: :cat: 

## Tips

最後に雑多な Tips を紹介します。

* 一時的に無効にする
    * スクリプトのデバッグ時など、一時的に定期実行を無効にしたい場合は、スケジュール設定の `Active` フラグで切り替えることができます
* エラーメールはスケジュール設定のオーナーだけに届く
    * ビルドが失敗した場合の通知メールは、スケジュール設定を登録したユーザー（オーナー）だけにしか送信されません
    * 監視スクリプトなど、ビルド失敗をチームとしてウォッチする必要がある場合は、別途チャットに通知するようにしておくなり、メールを自動転送するようにしておくなり、何かしらの対処が必要です
* 複数のスクリプトを管理する
    * 1つのリポジトリでスケジュールが異なる複数のスクリプトを管理したい場合は、スケジュール設定の環境変数をうまく組み合わせることで対応することができます

    ```yml:.gitlab-ci.yml
    image: alpine
    
    awesome-job:
      stage: build
      rules:
        - if: '$CI_PIPELINE_SOURCE == "schedule" && $AWESOME_JOB_SCHEDULE == "1"'
      script:
        - echo "Awesome job"

    great-job:
      stage: build
      rules:
        - if: '$CI_PIPELINE_SOURCE == "schedule" && $GREAT_JOB_SCHEDULE == "1"'
      script:
        - echo "Great job"
    ```


## さらに学ぶには

* [Get started with GitLab CI/CD | GitLab](https://docs.gitlab.com/ee/ci/quick_start/)
* [Pipeline schedules | GitLab](https://docs.gitlab.com/ee/ci/pipelines/schedules.html)
* [Keyword reference for the .gitlab-ci.yml file | GitLab](https://docs.gitlab.com/ee/ci/yaml/)

## まとめ

この記事では、GitLab CI を使って定期実行スクリプトを管理・実行する方法を紹介しました。ソースコードの管理と実行環境の管理を一元化することができ、とてもオススメです。

GitLab CI、イイよ！