CircleCI Runnerのセットアップ

## はじめに

この記事は[CircleCI Advent Calendar 2020](https://qiita.com/advent-calendar/2020/circleci)の5日目の記事です。

CircleCIでソリューションズエンジニアをしている車井と言います。

2020年11月17日にCircleCI Runner(以下、Runner)が[リリース](https://circleci.com/changelog/#circleci-runners)されました。
Runnerは自分・自社で管理しているサーバをCircleCIのビルドサーバとして利用することが出来る機能です。

本記事の執筆時点ではまだ日本語のマニュアルが出ていませんが、細かなユースケースや制限事項などは本家のマニュアルにおまかせするとして、ここではとりあえず動かしてみたいと思います。

~~タイミングよく、先日のAWS re:Invent 2020で、EC2にMacが加わったというニュースもありましたので、今回はLinuxとMacの両方をRunnerとして動かしてみたいと思います。~~
トライしてみましたが、Macのインスタンスがうまく作れなかったので、ここではLinuxだけをセットアップしてみます。

なお、実際にRunnerを使うには事前にCircleCIに問い合わせて、鍵開けしてもらう必要があります。
もし使ってみたいという方は、問い合わせて見てください。

## Runnerの構成要素

Runnerは下記の図に記載されているとおり、Launch AgentとTask Agentによって構成されています。
![Runner構成図](https://circleci.com/docs/assets/img/docs/runner-overview-diagram.png)

<dl>
<dt>ローンチ エージェント (launch-agent) </dt>
<dd>CircleCI本体と通信して必要な情報を収集したり、タスクエージェントを起動したりします。</dd>
<dt>タスク エージェント (task-agent) </dt>
<dd>1つ1つのタスクの実行を行います。</dd>
</dl>

すなわち、この2つのエージェントをセットアップすれば動くことになりますが、実際にはタスクエージェントはダウンロードしてきたものをそのまま動かすだけですので、ローンチエージェントの設定が主な作業になります。

なお、図でもわかるとおり、永続化が必要なデータ（キャッシュデータやアーティファクトなど）はCircleCIが管理しているストレージ領域に保存されるため、Runner専用の領域を別に作成する必要はありません。

## 事前準備

Runnerをセットアップするには、事前に名前空間、リソースクラス、認証トークンを作成しておく必要があります。

これらの作成にはCircleCI CLI(コマンドラインツール)をインストールしておく必要があります。CircleCI CLIのインストール方法はこちらの[マニュアル](https://circleci.com/docs/ja/2.0/local-cli/#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)を参照してください。

まず、名前空間を作成します。名前空間はVCS(GitHubやBitbucket)上の組織に対して一意に割り当てることができる名前です。

```
# circleci create <名前空間名> <VCS種別> <Org名>
```

VCS種別は"github"か"bitbucket"を指定しますので、例えば私の https://github.com/kurumai というOrgに対して "kurumai"という名前空間を作成する場合は下記のコマンドを実行します。

```
# circleci namespace create kurumai github kurumai
```

これで名前空間が出来ましたので、次にリソースクラス名を登録します。

```
# circleci runner resource-class create <resource-class> <description>
```

`<resource-class>`には"名前空間/リソースクラス名"の書式で指定します。
リソースクラス名は任意で構いませんが、CircleCIのconfig.ymlで実行環境として指定する値になりますので、サーバの種類や特性を表す文字列が分かりやすいと思います。
今回はCircleCIの標準の実行環境にはない、Armベースのプロセッサを搭載しているAWS EC2の`a1.medium` をリソースクラスとして登録してみたいので、`arm-runner`という名前で登録しました。

```
# circleci runner resource-class create kurumai/arm-runner "Arm runner"
+--------------------+-------------+
|   RESOURCE CLASS   | DESCRIPTION |
+--------------------+-------------+
| kurumai/arm-runner | Arm runner  |
+--------------------+-------------+
```

最後に、これらのリソースクラスを認証するためのトークンを作成しておきます。

```
# circleci runner token create <resource-class> <nickname>
```

nicknameは任意で構いません。
ここではリソースクラスと同じ名前を使っています。

```
# circleci runner token create kurumai/arm-runner arm-runner
api:
    auth_token: ********
```

これで事前準備は完了です。

## Arm Runnerのセットアップ

まず、 `a1.medium` タイプのEC2インスタンスにRunnerをセットアップします。OSはUbuntuで東京リージョンに作成しています。
セキュリティグループはアウトバウンドはすべて許可、インバウンドはセットアップのためにSSH(22)だけを許可しています。

RunnerはCircleCIに対してポーリングしてジョブを実行しますので、CircleCI側から(インターネット側から)の通信は許可する必要はありません。

それでは、SSHでログインして、まずはエージェントモジュールをダウンロードします。
いきなりいろいろなものを実行していますが、1行目の `platform=linux/arm64` だけ環境によって変更する必要がありますが、それ以外はマニュアルをそのままコピー＆ペーストすれば動くと思います。

```	
platform=linux/arm64
prefix=/opt/circleci
sudo mkdir -p "$prefix/workdir"
base_url="https://circleci-binary-releases.s3.amazonaws.com/circleci-launch-agent"
echo "Determining latest version of CircleCI Launch Agent"
agent_version=$(curl "$base_url/release.txt")
echo "Using CircleCI Launch Agent version $agent_version"
echo "Downloading and verifying CircleCI Launch Agent Binary"
curl -sSL "$base_url/$agent_version/checksums.txt" -o checksums.txt
file="$(grep -F "$platform" checksums.txt | cut -d ' ' -f 2)"
file="${file:1}"
mkdir -p "$platform"
echo "Downloading CircleCI Launch Agent: $file"
curl --compressed -L "$base_url/$agent_version/$file" -o "$file"
echo "Verifying CircleCI Launch Agent download"
sha256sum --check --ignore-missing checksums.txt && chmod +x "$file"; sudo cp "$file" "$prefix/circleci-launch-agent" || echo "Invalid checksum for CircleCI Launch Agent, please try download again"
```

次に `/opt/circleci/launch-agent-config.yaml` オーナーをroot、パーミッションは600で作成します。

```text:/opt/circleci/launch-agent-config.yaml
api:
  auth_token: AUTH_TOKEN <- こちらに先程取得したトークン文字列を記載します
runner:
  name: RUNNER_NAME <- こちらは任意の名前で構いません
  command_prefix: ["/opt/circleci/launch-task"]
  working_directory: /opt/circleci/workdir/%s
  cleanup_working_directory: true
```

エージェントを実行するためのユーザーを作成します。
ここでは `circleci` というユーザーを作成しています。

```
id -u circleci &>/dev/null || adduser --uid 1500 --disabled-password --gecos GECOS circleci

mkdir -p /opt/circleci/workdir
chown -R circleci /opt/circleci/workdir
```

ローンチエージェントのスクリプトを `/opt/circleci/launch-task` とし、オーナーをroot、パーミッションは755で作成します。

```text:/opt/circleci/launch-task
#!/bin/bash

set -euo pipefail

## This script launches the build-agent using systemd-run in order to create a
## cgroup which will capture all child processes so they're cleaned up correctly
## on exit.

# The user to run the build-agent as - must be numeric
USER_ID=$(id -u circleci)

# Give the transient systemd unit an inteligible name
unit="circleci-$CIRCLECI_LAUNCH_ID"

# When this process exits, tell the systemd unit to shut down
abort() {
  if systemctl is-active --quiet "$unit"; then
    systemctl stop "$unit"
  fi
}
trap abort EXIT

systemd-run \
    --pipe --collect --quiet --wait \
    --uid "$USER_ID" --unit "$unit" -- "$@"
```

ローンチエージェントをサービスとして起動するため、`/opt/circleci/circleci.service`ファイルを作成します。先程と同様にオーナーはroot、パーミッションは755で作成します。

```text:/opt/circleci/circleci.service
[Unit]
Description=CircleCI Runner
After=network.target
[Service]
ExecStart=/opt/circleci/circleci-launch-agent --config /opt/circleci/launch-agent-config.yaml
Restart=always
User=root
NotifyAccess=exec
TimeoutStopSec=18300
[Install]
WantedBy = multi-user.target
```
サービスを起動します。

```
systemctl enable /opt/circleci/circleci.service
```

正常に動いているかどうか確認しておきます。

```
systemctl start circleci.service
```

これでセットアップは完了です。
それでは、正しく動くかどうかチェックしてみます。
CircleCIの `config.yml` を下記のように作って動かしてみました。

Runnerを利用する場合は、`machine`に`true`を設定し、`resource_class`に作成した名前を指定します。

```yaml:config.yml
version: 2
jobs:
  prepare:
    docker:
      - image: circleci/node:latest
    steps:
      - checkout
      - run:
          name: Generate temp file
          command: |
            mkdir -p tmp
            echo "Hello CircleCI!!" > tmp/hello.txt
      - persist_to_workspace:
          root: .
          paths:
            - "tmp"
  arm-runner:
    machine: true
    resource_class: kurumai/arm-runner
    steps:
      - checkout
      - attach_workspace:
          at: .
      - run: cat tmp/hello.txt

workflows:
  version: 2
  runner-workflow:
    jobs:
      - prepare
      - arm-runner:
          requires:
              - prepare
```

結果です。なんとかちゃんと動きました。

<img src="https://github.com/kurumai/qiita/raw/circleci-runner-with-mac-on-aws/runner-workflow.png" width="70%" height="70%" >

<img src="https://github.com/kurumai/qiita/raw/circleci-runner-with-mac-on-aws/arm-runner.png" width="70%" height="70%" >


もしジョブがキューの状態で動かない場合は、サービスがちゃんと動いているのかや、設定を変更した場合はサービスを再起動したかどうかを確認すると良いと思います。
実際、私も設定を変更したにも関わらず、サービスを再起動するのを忘れてはまりかけました。

## おわりに

CircleCI Runnerをセットアップして、通常のCircleCIのExecutorと組み合わせたワークフローを動かしてみました。
セットアップは手作業が伴いますが、それほどはまるところは多くはないと思いますので、ぜひ興味のあるかたはトライしてみてください。

Runnerをうまく使うことで、スケーラビリティが必要なテストはCircleCIの標準のものを使い、セキュリティが気になるデプロイのところだけRunnerを使うということが簡単にできるようになりました。
また、これまではテストが難しかった組み込み系のシステムでもRunnerは活躍できるのではないかと思っています。

CI/CDはWebやモバイルアプリのサービス開発を中心にSaaS利用が広まっていますが、まだまだソフトウェア開発の世界にはCI/CDに着手できていないところがいっぱいあると思います。ぜひ、この機会にCircleCIを触ってみてCI/CDの世界に触れてみてください。