古き良きJenkinsからの卒業

[謎の集団Zeneloのアドベントカレンダー](https://qiita.com/advent-calendar/2018/zenelo)12月9日の記事です。 
今回は謎の集団Zeneloが日々利用しているCIツールである[Jenkins](https://jenkins.io/)が、古き良きJenkinsに育つまでの経緯と、そこからの卒業に向けて試している事を紹介します。

今回の記事は、Jenkinsを実際に利用している人向けに書いたものとなっており、若干説明不足な部分もありますが、ご容赦ください。

## 古き良きJenkinsが育つまで

- Jenkinsの導入背景
  - 繰り返し行う作業の中で、自動化できるものをJenkinsで実行したい。あれこれも色々とできるといいなぁ

![image.png](https://qiita-image-store.s3.amazonaws.com/0/19911/204cddc7-aad8-eb47-10ca-ce8a81589bd1.png)

### 0〜1年目

- 構築方法
 - [Terraform](https://www.terraform.io/)でAWSのリソースを管理、[Ansible](https://www.ansible.com/)で構成管理
- Jenkinsにお願い！
  - applicationのビルド
  - applicationのデプロイ
  - applicationのテスト

### 2〜3年目

- 管理・更新方法
  - Terraform/Ansible
- 管理者交代
- Jenkinsにお願い！
  - databaseのマイグレーション
  - GitHubのpull request単位でのテスト

### 4年目〜

- 管理・更新方法
  - Terraform/Ansible
- 管理者交代
    - そろそろゼロからJenkinsを作り直した方がいいかもしれない、と思い始める。しかし、いざ構築しようとするとAnsibldeで固定でいれていたversionのpackageがなかったりして、Ansibleの修正が必要だったり、色々あった。
- Jenkinsにお願い！
  - パフォーマンステスト
  - 新しい方法でのE2Eテスト

# 結論、そろそろ卒業したいです....

## 古き良きJenkinsについて

Jenkinsは、CI/CDのツールとしては比較的初期からあり、草分け的な存在です。
ただ、長年運用していると、どんどん色んな作業がJenkinsに任されてきます。

しかし、ここでJenkins上で直接処理を実行する方向に進んでいくと**古き良きJenkins**が育ちます。
例えるなら、最初は肉と豆腐と野菜のシンプルな鍋だったのに、次々と色々な食材が追加されていき、最終的には闇鍋が出来上がるような感じです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/19911/0229f5a9-759e-3384-0d5d-e24a7fcf8a5d.png)→![image.png](https://qiita-image-store.s3.amazonaws.com/0/19911/2c4bdb7f-71aa-aff5-ab76-3c53c8cce9e5.png)→![image.png](https://qiita-image-store.s3.amazonaws.com/0/19911/e0a6aa5a-8cbe-1b81-e704-d45d190ee0ee.png)

Jenkinsはシステムの中の構成要素の一つです。なんでもできると思われがちですが、結局のところはシステムを構成するサブシステムの一つにすぎません。様々な役割を任せやすい反面、一つのサブシステムに複数の役割を任せるのは、システム全体の複雑性を増すことに繋がり、SPOFを考慮しても、あまり好ましくない状態です。

ここまでネガティブ感があふれていますが、当然ですがツールに責任はなく、色々責務を任せすぎた設計・管理方法に問題があったということです。少なくともJenkins上で、テストやビルドを回す必要性はなく、大事なのは、 **継続的に自動で実行される** ことです。

そしてそろそろ卒業したいなぁという思いも本音のため、Jenkinsが担っている複数の役割を一つずつ見直していき、試しにテストの一部を別のサブシステムで実行しようと考えました。

### テストを再現可能な環境で実行する

上述した設計・管理方法の問題を踏まえ、次に構築する環境は一つの役割のみに集中させるため、テストをDockerで実行することにしました。
まずは、実行環境をDockerで準備するのですが、下記に簡単なサンプルをまとめます。実コードの抜粋となっているため、若干不自然な部分もありますが、やりたいことが伝われば幸いです。

#### Dockerfile => Jenkinsが担保していたテストの実行環境

```dockerfile

FROM node

RUN echo 'deb http://dl.google.com/linux/chrome/deb/ stable main' > /etc/apt/sources.list.d/chrome.list
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -

RUN apt-get update && apt-get install -y google-chrome-stable && apt-get clean

RUN mkdir /app
WORKDIR /app


ENV CHROME_BIN /usr/bin/google-chrome
COPY entrypoint.sh /
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
```

#### entrypoint.sh => Jenkinsのジョブが担保していたテストの実行処理

```sh
#!/bin/bash -e

WORKSPACE=/app
cd "$WORKSPACE"
npm cache verify
npm install
npm run lint
npm run build
npm run test
```

### テストの実行 => Docker化したのでコマンド一つで、ほぼどこでもテストが実行可能

```sh
docker run -v $(APP_DIR):/app $(IMAGE_NAME)
```

### テストの処理をお引越し

Docker化したことにより、上述したテストは **docker runできる環境ならどこでも実行可能** となりました。ポータブリティの面で、これまでよりは良い感じになったと思います。

ここまできたら、載せ替え先は正直選びたい放題ですが、今回はAWSが提供している[CodeBuild](https://aws.amazon.com/jp/codebuild/)を利用した形を紹介します。CodeBuildを簡単に説明すると、事前に設定しておいたリソースとYAMLファイルに定義された処理を利用する時のみ、確保・実行してくれるフルマネージドなビルドサービスです。CodeBuidの詳細な使い方については、[公式のドキュメント](https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/getting-started.html)を参考にしていただければと思います。


実際にCodeBuildで利用するYAMLファイルを定義するとしたら、以下のような形になります。

```yaml
version: 0.2

env:
  variables:
    AWS_DEFAULT_REGION: ap-northeast-1
    AWS_ACCOUNT_ID: xxxxxxxxxxxx

phases:
  pre_build:
    commands:
      - echo $CODEBUILD_SOURCE_VERSION start
      - echo Logging in to Amazon ECR...
      - aws --version
      - $(aws ecr get-login --no-include-email --region ${AWS_DEFAULT_REGION})
      - docker pull ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/test
  build:
    commands:
      - echo Building the Docker image...
         - docker run -v $(APP_DIR):/app $(IMAGE_NAME)
  post_build:
    commands:
      - echo $CODEBUILD_SOURCE_VERSION passed

```

今回の実作業としては、Docker環境の準備と、CodeBuildの設定が八割です。補足すると紹介させていただいたものはフロントエンド部分のテスト環境となりますが、実際にはサーバサイドとフロントエンドそれぞれで違うDocker Imageを準備しました。一つの環境には一つの役割だけを持たせるといった方針をとった形です。

今回はテスト部分の切り出しでしたが、やってみると非常にシンプルに切り出すことができました。既存の役割が多いとはいえ、一つずつ着実に片付けていけば、卒業できるという確信が持てました。来年あたりには、卒業しました宣言を出したいです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/19911/b7785373-8192-b43f-a3bf-fe09b558c5a7.png)