GitLab CIとdockerでRestAPIサーバーのテストとデプロイを自動化する

GitHubの買収騒動のときに代替手段としてすこし話題になったGitLabですが、せっかくCI機能があるので使ってみようと思います。
目標は、以下をできるようになることです。

1. GitLabにRestAPIサーバーのソースコードをcommitする
2. ソースコードのビルドが走り、以下のJobを自動で実行してくれる
    1. ソースコードをdocker imageにしてGitLabのDocker Registryに登録する
    2. 作ったimageをローカルサーバーにデプロイする
    3. デプロイしたDocker containerに対してPostMan(newman)のtest jobを走らせて結果を確認する

イメージとしては以下のような感じですね

![Untitled Diagram (1).png](https://qiita-image-store.s3.amazonaws.com/0/93999/e3bb309e-6d8a-983b-4bec-914e038a93c3.png)




GitLabを使う利点としては、

* Git repositoryと Container Registry とCIがセットになっている
* Runner Serverにローカルマシンを使える(Gitlabに対してRunnerを公開する必要がない)
* GitLab自体がOSSなのでオンプレでも立てられる

があります。 ~~特に一番下のやつは、企業コンプライアンス的に外のサーバー使っちゃだめよってところでも使えるので助かります~~

一応、CI/CDはなんとなく知ってるけど、GitLab CIは知らないよ。って人向けに書いたつもりです。けっこう長くなっちゃいましたが・・・

## GitLabでリポジトリを作る

GitLabそのものをローカルサーバーにたてて使うこともできますが、今回はgitlab.comでパブリックリポジトリを作ることにします。

以下にテスト用のリポジトリを作りました。
https://gitlab.com/kuwabataK/Rest-API-Test-Server

今回の成果物なんかも全部あげてあるので、参考にしてください。

## GitLab Runnner の登録

GitLabのCI機能を使うためには、Jobを実行するためのRunnerサーバーが別に必要になります。
公式のShared Runnnerを使うこともできるようですが、今回は自分のローカルPC上に立てたLinux VMを
RunnerServerとして使いたいと思います。

### GItLab Runnerのインストール

下記の手順に従ってRunnerを用意したVM(Ubuntu16.04)にインストールします。

https://docs.gitlab.com/runner/install/linux-repository.html


``` shell
$ curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash
```

``` shell
$ sudo apt-get install gitlab-runner
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following packages were automatically installed and are no longer required:
  linux-headers-4.4.0-104 linux-headers-4.4.0-104-generic
  linux-headers-4.4.0-119 linux-headers-4.4.0-119-generic
  linux-headers-4.4.0-121 linux-headers-4.4.0-121-generic
  linux-image-4.4.0-104-generic linux-image-4.4.0-119-generic
  linux-image-4.4.0-121-generic
Use 'sudo apt autoremove' to remove them.
Suggested packages:
  docker-engine
The following NEW packages will be installed:
  gitlab-runner
0 upgraded, 1 newly installed, 0 to remove and 79 not upgraded.
Need to get 26.5 MB of archives.
After this operation, 48.9 MB of additional disk space will be used.
Get:1 https://packages.gitlab.com/runner/gitlab-runner/ubuntu xenial/main amd64 gitlab-runner amd64 11.0.0 [26.5 MB]
Fetched 26.5 MB in 4s (6,294 kB/s)
Selecting previously unselected package gitlab-runner.
(Reading database ... 171662 files and directories currently installed.)
Preparing to unpack .../gitlab-runner_11.0.0_amd64.deb ...
Unpacking gitlab-runner (11.0.0) ...
Setting up gitlab-runner (11.0.0) ...
GitLab Runner: creating gitlab-runner...
gitlab-runner: Service is not installed.
gitlab-ci-multi-runner: Service is not installed.
Clearing docker cache...
```

なお、v10.0以前とそれ以後ではRunnerのコマンドが変わっています。`gitlab-ci-multi-runner` ⇛ `gitlab-runner`になった。

v10.0以前からアップデートする場合は、以下などを参照してください
https://blog.n-z.jp/blog/2017-11-06-gitlab-runner.html

### GitLabRunnerのプロジェクトへの登録

GitLabプロジェクト⇛`Setting`⇛ `CI/CD` ⇛ `Runners`と開いていくと以下のような画面が出てくるので、
`Setup a specific Runner manually`に書かれた手順に従って実行していきます

![2018-07-14_18h56_31.png](https://qiita-image-store.s3.amazonaws.com/0/93999/1fdb1710-3bff-af15-0e91-18e6d7d9b846.png)


以下を実行します。

``` shell
$ sudo gitlab-runner register
Running in system-mode.

Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
https://gitlab.com
Please enter the gitlab-ci token for this runner:
XXXXXXXXXXXXXXXXXXXXXX // Tokenを指定
Please enter the gitlab-ci description for this runner:
test-runner　//このRunnerの説明空欄でも良い
Please enter the gitlab-ci tags for this runner (comma separated):
test-runner // このRunnerにつけるタグ
Registering runner... succeeded                     runner=XXXXXXX
Please enter the executor: kubernetes, docker-ssh, ssh, virtualbox, docker-ssh+machine, docker, parallels, shell, docker+machine:
shell // Jobの実行環境を指定できる。今回は無難にShellで、そのうちKubernetesとかも使ってみたい
Runner registered successfully. Feel free to start it, but if it's running already the config should be automatically reloaded!
```

以上で完了です。

登録がうまくいくと、以下のようにRunnerがProjectに登録されます

![2018-07-14_19h31_59.png](https://qiita-image-store.s3.amazonaws.com/0/93999/917b8578-7c6d-58fa-bf12-d44ead6f1a22.png)

## Jobを作る

うまく動くかどうか、ためしにJobを作ってみます。

Gitのプロジェクト直下に以下のような`.gitlab-ci.yml`ファイルを作ってみます。


``` ..gitlab-ci.yml
test-job:
  tags:
    - test-runner                            // Runnnerを指定
  script:
    - date                                   // 実行するコマンド。今回は現在時刻を表示するだけ 
```

でこいつをGitLabのリポジトリにプッシュしてやれば、自動でJobが走るはずです。試してみます。

```
$ git add .gitlab-ci.yml
$ git commmit -m "initial commit"
$ git push origin master
```

`CI/CD` ⇛ `Pipeline` を表示してやると・・・

![2018-07-14_19h41_34.png](https://qiita-image-store.s3.amazonaws.com/0/93999/e1a9872f-f23b-702a-9a95-5c5516b8dd8b.png)

動いてます！
https://gitlab.com/kuwabataK/Rest-API-Test-Server/pipelines

Jobの中身を表示してやると以下のような感じです。ちゃんと実行されていますね

![2018-07-14_19h42_06.png](https://qiita-image-store.s3.amazonaws.com/0/93999/cf75ad2a-5d57-a7e4-e907-e2a8382f83a6.png)

以上でGItLabCI側の準備は完了です。


## Flask Restlessで簡単にAPIサーバーを作る

というわけで、サクッとテスト対象のAPIサーバーを作りたいと思います。
今回はFlaskベースの[Flask Restless](https://flask-restless.readthedocs.io/en/stable/)を使うことにします。

詳しくは説明しませんが、DBのModelを定義してあげるだけで、そのModelへのCRUD操作を実現するエンドポイントを自動で作ってくれるすごいやつです。DBへアクセスするだけの簡単なエンドポイントを作るだけならめちゃめちゃ便利なのでよく使っています。最近流行りのGraphQLほどの柔軟性はありませんが、
クエリに応じてレスポンスにフィルタをかけれたりする(例えば誕生日が10月の人だけ拾ってくるとか)のでよいです。 **流行れ**

以下の3つのファイルを作ります。プロジェクトの構成は以下のような感じ `app.py` は[公式のサンプル](https://github.com/jfinkels/flask-restless/blob/master/examples/quickstart.py)をそのまま使わせてもらってます。

```
.
├── .getlab-ci.yml
├── app.py
├── README.md
├── requirements.txt
└── run.sh

```

``` app.py
import flask
import flask_sqlalchemy
import flask_restless

# Create the Flask application and the Flask-SQLAlchemy object.
app = flask.Flask(__name__)
app.config['DEBUG'] = True
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
db = flask_sqlalchemy.SQLAlchemy(app)


# Create your Flask-SQLALchemy models as usual but with the following
# restriction: they must have an __init__ method that accepts keyword
# arguments for all columns (the constructor in
# flask_sqlalchemy.SQLAlchemy.Model supplies such a method, so you
# don't need to declare a new one).
class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Unicode)
    birth_date = db.Column(db.Date)


class Article(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.Unicode)
    published_at = db.Column(db.DateTime)
    author_id = db.Column(db.Integer, db.ForeignKey('person.id'))
    author = db.relationship(Person, backref=db.backref('articles',
                                                        lazy='dynamic'))


# Create the database tables.
db.create_all()

# Create the Flask-Restless API manager.
manager = flask_restless.APIManager(app, flask_sqlalchemy_db=db)

# Create API endpoints, which will be available at /api/<tablename> by
# default. Allowed HTTP methods can be specified as well.
manager.create_api(Person, methods=['GET', 'POST', 'DELETE'])
manager.create_api(Article, methods=['GET'])

# start the flask loop
app.run()
```

```requirements.txt
flask
flask-sqlalchemy
flask_restless
sqlalchemy
python-dateutil

```

``` run.sh
#!/bin/bash

FLASK_APP=app.py
flask run --host=0.0.0.0
```

`app.py`について軽く解説しときます。

DBへの接続情報は以下で指定しています。今回は自分で作成したDBに接続していますね。
一応postgresとか、メジャーなDBへ接続するドライバはだいたい用意されているので本番環境でも使えます

```
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
```

データベースへの接続モデルは以下のところで定義しています。
ここの構造がDBのテーブル構造と合わないとエラーになっちゃうので注意してください。
ただ、試した感じDBの型はそこまで厳密に見てないっぽいです(Date型で入っているデータをStringで拾えたりする)

```
class Person(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Unicode)
    birth_date = db.Column(db.Date)


class Article(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.Unicode)
    published_at = db.Column(db.DateTime)
    author_id = db.Column(db.Integer, db.ForeignKey('person.id'))
    author = db.relationship(Person, backref=db.backref('articles',
                                                        lazy='dynamic'))
```


APIを作るのは以下ですね。許可するメソッドとかもここで指定しています。

```
manager.create_api(Person, methods=['GET', 'POST', 'DELETE'])
manager.create_api(Article, methods=['GET'])
```

詳しくは<https://flask-restless.readthedocs.io/en/stable/>を見てください。

実行は以下です。Webサーバーが立ち上がります。

``` shell
$ pip install -r requirements.txt
$ sh ./run.sh
```

以下のURLにリクエストを送るとPersonテーブルの中身が帰ってきます。(まあ最初なので中身は空だと思いますが)
http://localhost:5000/api/person

## Dockerコンテナの作成

動作が確認できたところで、このサーバーをコンテナにしてGitLabのRegistryに登録していきます。
事前にRunner Serverにdockerとdocker-composeをインストールしておいてください

新たに`docker`ディレクトリを作り、以下のような`Dockerfile`を作ります。

``` Dockerfile
FROM python:3.6
WORKDIR /path/to/dir
RUN git clone https://gitlab.com/kuwabataK/Rest-API-Test-Server.git
WORKDIR Rest-API-Test-Server
RUN pip install -r requirements.txt
CMD [ "sh", "run.sh" ]
```

リポジトリのところは適宜書き換えてください。

ついでにデプロイ用の`docker-compose.yml`ファイルも作っちゃいます。

``` docker-compose.yml
version: '3'
services:
  rest-api-test-server:
    image: registry.gitlab.com/kuwabatak/rest-api-test-server
    ports:
      - "15000:5000"
    restart: always
```

``` ..ディレクトリ構成
.
├── app.py
├── docker
│   ├── docker-compose.yml
│   └── Dockerfile
├── README.md
├── requirements.txt
└── run.sh

```

## Runnerでビルドを走らせる

さて、Dockerfileができたところで、`gitlab-ci.yml`にbuild jobを書くわけですが、ここで注意点があります。

Runnerを登録するときに`Shell`を選んでいる場合、runnerの実行ユーザーとして`gitlab-runner`というユーザーが追加されています。
デフォルトではDockerはrootユーザーでしか実行できないので、以下を実行してこのユーザーでDockerを使えるようにします。

```
$ sudo usermod -aG docker gitlab-runner
```

詳しくは、[Shell | Gitlab](https://docs.gitlab.com/runner/executors/shell.html)を参照してください。

また、このユーザーでContainer imageをGitLabのRegistryに登録するためには、事前に`docker login`しておく必要があります。
このユーザーでログインするために、まずは`gitlab-runner`ユーザーのパスワードを書き換えましょう

``` shell
$ sudo passwd gitlab-runner
```

書き換えたパスワードで`gitlab-runner`にログインし、`docker login`を実行します。

``` shell
$ su gitlab-runner
$ docker login registry.gitlab.com             // registry.gitlab.com 部分は環境に応じて適宜書き換え
```

準備ができたところで、`gitlab-ci.yml`を書き換えていきます。

``` ..gitlab-ci.yml

stages:
  - build
  - deploy

build:
  stage: build
  tags:
    - test-runner
  script:
    - cd ./docker
    - docker build -t registry.gitlab.com/kuwabatak/rest-api-test-server .
    - docker push registry.gitlab.com/kuwabatak/rest-api-test-server

deploy:
  stage: deploy
  tags:
    - test-runner
  script:
    - cd ./docker
    - docker-compose down
    - docker-compose up -d

```

`build` と `deploy` という2つのステージに分かれてjobが実行されます。コンテナのビルドが終わったらRunnerにデプロイして15000ポートで公開するって感じの流れですね。
ついでにGitLab Registryへのイメージ登録も行っています。
登録されたコンテナイメージは`Registry`から確認できます。
https://gitlab.com/kuwabataK/Rest-API-Test-Server/container_registry

Jobが成功したら、いかに接続してみてちゃんとデプロイされたかどうか確認してみます。

```
http://<Runner-host>:15000/api/person
```

以上です。これで成果物の自動デプロイ環境ができたので、デプロイしたサーバーに対してPostman(newman)を使ったテストを作っていきたいと思います。

## PostmanでWebAPIテストを作る

PostmanはChromeアプリとして動くRestClientの一つです。GUIベースでサクサクWebAPIに対するテストが作れ、作成したテストコードをまとめて実行することができます。

イメージはこんな感じですね

![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/93999/fa9b467e-782b-8522-af43-c6e202d33541.png)

![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/93999/0e193f86-b096-57d7-95ba-e23ffa180ca7.png)

細かな使い方は割愛しますが、基本的にはRestClientなので、URL、メソッド、リクエストヘッダやリクエストボディをしてしてあげてリクエストを投げることができます。

また、リスポンスに対するテスト(ステータスコードやレスポンスボディの形式が正しいかどうかなど)を行うことができます。

また以下のような感じで作ったテストを順番に実行してみることもできます。

![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/93999/a9ab9ecb-aeab-5fcb-2331-ab24fca5a20a.png)


今回はこれを使って、以下のような流れのテストを作成したいと思います。


1. /api/personに対するGETリクエストがうまくいくかどうか(テスト①)
2. /api/personに対するPOSTリクエスト(Personデータの作成)がうまくいくかどうか(テスト②)
3. /api/personにGETリクエストを投げてさっき投入したデータが入っているかどうか(テスト③)


さて、このPostmanですが、Postmanで作成したテストデータをCLIベースで実行することのできるnewmanというツールがあります。

今回はこのnewmanを使ってテストを自動化していきたいと思います。
以下を実行してnewmanをプロジェクトに導入します。

```
$ npm init
$ npm install --save newman
```

次にPostmanで作成したテストをエクスポートしましょう。
エクスポートしたファイルは以下のような感じ。json形式で
保存されるっぽいですね

``` REST-API-TEST.postman_collection.json
{
	"variables": [],
	"info": {
		"name": "REST-API-TEST",
		"_postman_id": "8efba306-eefc-f9e3-2204-0b808885ce9c",
		"description": "",
		"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json"
	},
	"item": [
		{
			"name": "get_person",
			"event": [
				{
					"listen": "test",
					"script": {
						"type": "text/javascript",
						"exec": [
							"tests[\"Status code is 200\"] = responseCode.code === 200;"
						]
					}
				}
			],
			"request": {
				"url": "http://{{SERVER_HOST}}/api/person",
				"method": "GET",
				"header": [],
				"body": {},
				"description": ""
			},
			"response": []
		},
		{
			"name": "post_person",
			"event": [
				{
					"listen": "test",
					"script": {
						"type": "text/javascript",
						"exec": [
							"tests[\"Successful POST request\"] = responseCode.code === 201 || responseCode.code === 202;"
						]
					}
				}
			],
			"request": {
				"url": "http://{{SERVER_HOST}}/api/person",
				"method": "POST",
				"header": [
					{
						"key": "Content-Type",
						"value": "application/json",
						"description": ""
					}
				],
				"body": {
					"mode": "raw",
					"raw": "{\n\t\"name\": \"KuwabataK\",\n\t\"birth_date\": \"1990-10-25\",\n\t\"articles\": [\n\t\t{\n\t\t\t\"title\": \"GitLab CIでAPI Serverのデプロイとテストを自動化したい\",\n\t\t\t\"published_at\": \"2018-07-14\"\n\t\t}\n\t\t]\n}"
				},
				"description": ""
			},
			"response": []
		},
		{
			"name": "get_person_after_create_user",
			"event": [
				{
					"listen": "test",
					"script": {
						"type": "text/javascript",
						"exec": [
							"tests[\"Status code is 200\"] = responseCode.code === 200;",
							"",
							"",
							"var jsonData = JSON.parse(responseBody);",
							"tests[\"ちゃんとユーザーデータが入っていることをテスト\"] = jsonData.objects[0].name === \"KuwabataK\";"
						]
					}
				}
			],
			"request": {
				"url": "http://{{SERVER_HOST}}/api/person",
				"method": "GET",
				"header": [],
				"body": {},
				"description": ""
			},
			"response": []
		}
	]
}
```

``` gitlab-ci-env.postman_environment.json
{
  "id": "e2fd1cb5-1fc3-16d9-3554-3f53589f8f9b",
  "name": "gitlab-ci-env",
  "values": [
    {
      "key": "SERVER_HOST",
      "value": "localhost:15000",
      "description": "",
      "type": "text",
      "enabled": true
    }
  ],
  "timestamp": 1531616910454,
  "_postman_variable_scope": "environment",
  "_postman_exported_at": "2018-07-15T01:47:44.747Z",
  "_postman_exported_using": "Postman/5.5.3"
}
```
 `REST-API-TEST.postman_collection.json`がテスト内容を記述したファイルになります
PostmanでExportしたファイルなので若干わかりにくいデータ構造になっていますが、

`"name": "get_person"` `"name": "post_person"` `"name": "get_person_after_create_user"`の3つのテストがあることがわかるかと思います。
これを上から順番に流していくって感じですね。

例えば`post_person`では以下のようなリクエストを投げてます。

```
method: POST
Endpoint: /api/person
```

``` requestbody.json
{
	"name": "KuwabataK",
	"birth_date": "1990-10-25",
	"articles": [
			{
				"title": "GitLab CIでAPI Serverのデプロイとテストを自動化したい",
				"published_at": "2018-07-14"
			}
		]
}

```

`gitlab-ci-env.postman_environment.json`はテスト用の環境変数設定ファイルです。テストの中で`http://{{SERVER_HOST}}/api/person`になっている部分があるかと思うのですが、
このファイルを使うことで、ここの`{{SERVER_HOST}}`に `localhost:15000`(Runnerサーバーから見えるAPI-TEST-Serverのアドレス) を差し込む事ができます。
要するに複数の環境で変数を変えながらテストできるってことですね


`api-test`ディレクトリを作ってエクスポートしたファイルをに突っ込みます

テストを実行するために`package.json`を以下のように書き換えます。

```package.json
...
"scripts": {
    "test": "newman run ./api-test/REST-API-TEST.postman_collection.json -e ./api-test/gitlab-ci-env.postman_environment.json"
  },
...
```

試しにテストが実行されるかどうか、以下を実行して確かめてみましょう

```
$ npm test
```

## テストをCIに組み込む

テストをCIに組み込みます。`.gitlab-ci.yml`を以下のように書き換えます。Runnerサーバーにnode.jsがインストールされていないと失敗するので注意してください。
ついでにテストが成功した場合のみGitLabのContainer Registryが更新されるようにしました

```..gitlab-ci.yml

stages:
  - build
  - deploy
  - test
  - register

build:
  stage: build
  tags:
    - test-runner
  script:
    - cd ./docker
    - docker build --no-cache -t registry.gitlab.com/kuwabatak/rest-api-test-server .

deploy:
  stage: deploy
  tags:
    - test-runner
  script:
    - cd ./docker
    - docker-compose down
    - docker-compose up -d

test:
  stage: test
  tags:
    - test-runner
  script:
    - npm install
    - npm test

register:
  stage: register
  tags:
    - test-runner
  script:
    - cd ./docker
    - docker push registry.gitlab.com/kuwabatak/rest-api-test-server

```

ディレクトリ構成は以下のような感じ

```
.
├── api-test
│   ├── gitlab-ci-env.postman_environment.json
│   └── REST-API-TEST.postman_collection.json
├── app.py
├── docker
│   ├── docker-compose.yml
│   └── Dockerfile
├── package.json
├── package-lock.json
├── README.md
├── requirements.txt
└── run.sh


```

## 完成！！

さあ、これで完成です！
gitlabにcommitして結果を確認してみます！

```
$ git add .
$ git commmit -m "完成！"
$ git push origin master
```

![2018-07-15_16h14_06.png](https://qiita-image-store.s3.amazonaws.com/0/93999/6560f11a-7b2a-5cad-a2fb-48129d79900f.png)

走ってる走ってる。

ちなみにテスト結果は以下のような感じで見えます。

![キャプチャ.PNG](https://qiita-image-store.s3.amazonaws.com/0/93999/ba561917-9a86-9619-2eb1-b10fa77f5bb0.png)

いい感じですね。テストが失敗したらメールで通知が来るので、すぐわかります。ちゃんと設定してあげればSlack連携とかもできるのかな

## やり残したこと

以上で一通りテストまでできるようになったので終わりですが、他にも

* テストが成功したら本番環境にもデプロイする
* ビルド前にユニットテストなどを実行する
* test jobがRunnerVM上で直接動いていて、環境を汚す可能性がありあまりよろしくないので、test jobもDocker containerの上で実行するようにする

などなど、やり残したことは結構あります。

実際のプロジェクトでやるときはそこらへんも気をつけてやれるといいかな・・・と思います。

## 参考

[GitLab Continuous Integration (GitLab CI/CD)](https://docs.gitlab.com/ee/ci/README.html)