GitHubの買収騒動のときに代替手段としてすこし話題になったGitLabですが、せっかくCI機能があるので使ってみようと思います。
目標は、以下をできるようになることです。
- GitLabにRestAPIサーバーのソースコードをcommitする
- ソースコードのビルドが走り、以下のJobを自動で実行してくれる
- ソースコードをdocker imageにしてGitLabのDocker Registryに登録する
- 作ったimageをローカルサーバーにデプロイする
- デプロイしたDocker containerに対してPostMan(newman)のtest jobを走らせて結果を確認する
イメージとしては以下のような感じですね
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)にインストールします。
$ 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-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
に書かれた手順に従って実行していきます
以下を実行します。
$ 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に登録されます
Jobを作る
うまく動くかどうか、ためしにJobを作ってみます。
Gitのプロジェクト直下に以下のような.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
を表示してやると・・・
動いてます!
https://gitlab.com/kuwabataK/Rest-API-Test-Server/pipelines
Jobの中身を表示してやると以下のような感じです。ちゃんと実行されていますね
以上で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
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()
flask
flask-sqlalchemy
flask_restless
sqlalchemy
python-dateutil
#!/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
ファイルも作っちゃいます。
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
を書き換えていきます。
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に対するテストが作れ、作成したテストコードをまとめて実行することができます。
イメージはこんな感じですね
細かな使い方は割愛しますが、基本的にはRestClientなので、URL、メソッド、リクエストヘッダやリクエストボディをしてしてあげてリクエストを投げることができます。
また、リスポンスに対するテスト(ステータスコードやレスポンスボディの形式が正しいかどうかなど)を行うことができます。
また以下のような感じで作ったテストを順番に実行してみることもできます。
今回はこれを使って、以下のような流れのテストを作成したいと思います。
- /api/personに対するGETリクエストがうまくいくかどうか(テスト①)
- /api/personに対するPOSTリクエスト(Personデータの作成)がうまくいくかどうか(テスト②)
- /api/personにGETリクエストを投げてさっき投入したデータが入っているかどうか(テスト③)
さて、このPostmanですが、Postmanで作成したテストデータをCLIベースで実行することのできるnewmanというツールがあります。
今回はこのnewmanを使ってテストを自動化していきたいと思います。
以下を実行してnewmanをプロジェクトに導入します。
$ npm init
$ npm install --save newman
次にPostmanで作成したテストをエクスポートしましょう。
エクスポートしたファイルは以下のような感じ。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": []
}
]
}
{
"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
{
"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
を以下のように書き換えます。
...
"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が更新されるようにしました
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
走ってる走ってる。
ちなみにテスト結果は以下のような感じで見えます。
いい感じですね。テストが失敗したらメールで通知が来るので、すぐわかります。ちゃんと設定してあげればSlack連携とかもできるのかな
やり残したこと
以上で一通りテストまでできるようになったので終わりですが、他にも
- テストが成功したら本番環境にもデプロイする
- ビルド前にユニットテストなどを実行する
- test jobがRunnerVM上で直接動いていて、環境を汚す可能性がありあまりよろしくないので、test jobもDocker containerの上で実行するようにする
などなど、やり残したことは結構あります。
実際のプロジェクトでやるときはそこらへんも気をつけてやれるといいかな・・・と思います。