Edited at

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

More than 1 year has passed since last update.

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

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

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

$ 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-runnergitlab-runnerになった。

v10.0以前からアップデートする場合は、以下などを参照してください

https://blog.n-z.jp/blog/2017-11-06-gitlab-runner.html


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

GitLabプロジェクト⇛SettingCI/CDRunnersと開いていくと以下のような画面が出てくるので、

Setup a specific Runner manuallyに書かれた手順に従って実行していきます

2018-07-14_18h56_31.png

以下を実行します。

$ 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


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/CDPipeline を表示してやると・・・

2018-07-14_19h41_34.png

動いてます!

https://gitlab.com/kuwabataK/Rest-API-Test-Server/pipelines

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

2018-07-14_19h42_06.png

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


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

というわけで、サクッとテスト対象のAPIサーバーを作りたいと思います。

今回はFlaskベースのFlask Restlessを使うことにします。

詳しくは説明しませんが、DBのModelを定義してあげるだけで、そのModelへのCRUD操作を実現するエンドポイントを自動で作ってくれるすごいやつです。DBへアクセスするだけの簡単なエンドポイントを作るだけならめちゃめちゃ便利なのでよく使っています。最近流行りのGraphQLほどの柔軟性はありませんが、

クエリに応じてレスポンスにフィルタをかけれたりする(例えば誕生日が10月の人だけ拾ってくるとか)のでよいです。 流行れ

以下の3つのファイルを作ります。プロジェクトの構成は以下のような感じ app.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サーバーが立ち上がります。

$ 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を作ります。

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を参照してください。

また、このユーザーでContainer imageをGitLabのRegistryに登録するためには、事前にdocker loginしておく必要があります。

このユーザーでログインするために、まずはgitlab-runnerユーザーのパスワードを書き換えましょう

$ sudo passwd gitlab-runner

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

$ 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


builddeploy という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

キャプチャ.PNG

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

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

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

キャプチャ.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

走ってる走ってる。

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

キャプチャ.PNG

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


やり残したこと

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


  • テストが成功したら本番環境にもデプロイする

  • ビルド前にユニットテストなどを実行する

  • test jobがRunnerVM上で直接動いていて、環境を汚す可能性がありあまりよろしくないので、test jobもDocker containerの上で実行するようにする

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

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


参考

GitLab Continuous Integration (GitLab CI/CD)