PR

GitLabを用いた継続的インテグレーション

2019年4月25日(木)
藤原 涼馬
連載5回目となる今回は、GitLab CIを用いた継続的インテグレーション(CI)について解説します。

サーバサイドアプリケーションの実装とテスト

今回作成するのは、非常に単純なアプリケーションです。/api/pingのURLにHTTPのGETリクエストを行った際に、pongという値が含まれたJSONメッセージを含んだHTTPレスポンスを返す非常に単純なアプリケーションです(図7)。これを実装し動作確認していきましょう。今後は今回作成するAPIのことをPingPong APIと呼びます。

図7:PingPong APIの動作イメージ

図7:PingPong APIの動作イメージ

DjangoおよびDjango REST Frameworkの導入については、ここでは省略します。それぞれの導入方法の詳細については、Djangoの公式ドキュメントのクイックインストールガイドや、Django REST frameworkの公式ドキュメントのInsallationを参照してください。さらにPythonのバージョンの切り替えにpyenv、各種パッケージの導入環境の分離にvenv)を用いています。導入にはそれぞれの公式ドキュメントを参照してください。

サーバサイドアプリケーションの下準備

サーバサイドアプリケーションはDjangoを利用します。pipコマンドを用いて容易に導入が可能です(リスト2)。

リスト2:リスト2:Djangoのインストール

$ pip install django

Djangoのインストールが完了したら、Djangoのプロジェクトを作成します(リスト3)。

リスト3:リスト3:Djangoのsampleappプロジェクトの作成

$ mkdir server
$ cd server
$ django-admin startproject sampleapp

コマンドを実行したディレクトリにsampleappディレクトリが作成されているはずです。では、一度立ち上げて正常にDjangoのプロジェクトを起動できるかを確認しましょう(リスト4)。

リスト4:リスト4:動作確認用にsampleappプロジェクトを起動させる

$ cd sampleapp
$ python manage.py runserver
Performing system checks...

System check identified no issues (0 silenced).

You have 15 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

February 03, 2019 - 10:37:26
Django version 2.1.5, using settings 'sampleapp.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

それでは、http://127.0.0.1:8000にアクセスしてみましょう。下の図8に示すような画面が表示されれば、Djangoのプロジェクトは正常に構成されています。

図8:Djangoの初期画面

図8:Djangoの初期画面

Djangoのsampleappプロジェクトに、今回実装するアプリケーションのディレクトリを追加していきます。

リスト5:リスト5:RESTアプリケーションをDjangoのsampleappプロジェクトへ追加する

$ cd sampleapp
$ python manage.py startapp rest

Django REST frameworkと、ここで作成したRESTアプリケーションを、Django内部で利用できるようにserver/sampleapp/sampleapp/settings.pyを修正しましょう。INSTALLED_APPSの配列にrest_frameworkrestを追加します(リスト6)。また、合わせて外部からのアクセスを受け付けられるように、ALLOWED_HOSTS = ["*"]としておきます。

リスト6:リスト6:INSTALLED_APPSの修正(server/sampleapp/sampleapp/settings.pyから抜粋)

ALLOWED_HOSTS = ["*"]

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework', # Django REST framework
    'rest', # restアプリケーションの追加
]

今回のアプリケーションでは、日本語で利用することを想定しているため、言語設定とタイムゾーンを日本に指定しましょう。LANGUAGE_CODEjaTIME_ZONEAsia/Tokyoとします(リスト7)。

リスト7:リスト7:言語設定の変更(server/sampleapp/sampleapp/settings.pyから抜粋)

LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'

先ほど、python manage.py startppRESTアプリケーションを追加しました。リクエストがRESTアプリケーションにもルーティングできるように、server/sampleapp/sampleapp/urls.pyを修正します(リスト8)。

リスト8:リスト8:RESTアプリケーションへのルーティングの追加(server/sampleapp/sampleapp/urls.py)

from django.contrib import admin
from django.urls import path
from django.urls import include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include('rest.urls')), # RESTアプリケーションを参照するよう指定
]

これで、http://127.0.0.1:8000/apiのパス以下にアクセスする際は、RESTアプリケーションの配下のurls.pyにてリクエストのルーティング処理が行われるようになります。では、ルーティングが適切に行われるように、server/sampleapp/rest/urls.pyを作成しましょう(リスト9)。

リスト9:リスト9:server/sampleapp/rest/urls.py

from django.urls import path

urlpatterns = []

現時点ではルーティングも何も記述されていませんが、これでアプリケーション自体の骨格は完成しました。最初のコミットはこれからのアプリケーションの基礎となるのでリモートリポジトリのmasterブランチにコードをプッシュしましょう。これでようやくサーバアプリケーションの骨格が整いました。これ以降は実際のアプリケーションの作成を進めていきましょう。

PingPong APIの仕様整理とテストコード記述

実装を始める前に改めてPingPong APIの仕様を整理し、テストを記述していきましょう。テストを事前に記述することで以下のような効果が得られます。

  1. 実装のゴールの明確化
  2. 追加実装やリファクタリングに伴い破壊的な変更が存在しないかのチェック

今回は新規の実装なので、得られる効果は主に前者の「実装のゴールの明確化」になります。

仕様を明確にしてテスト記述に備える

さて、今回作成するPingPong APIの仕様ですが、図7のように、単純に図示できる程度のシンプルなものです。本格的に複雑なものを作成する際には、リクエストとレスポンスの関係をより詳細かつ正確に記述するために、OpenAPI Specificationの利用を検討しても良いでしょう※1

※1:Open API specification 3.0を利用する際にはSwaggerなどをコミュニケーションに活用すると良いかもしれません。

ただし、今回の事例では作成するものが非常にシンプルなので、特にそういったものは利用せずに実装を進めます。

図7:PingPong APIの動作イメージ(再掲)

図7:PingPong APIの動作イメージ(再掲)

リクエストの仕様を表2に示します。/api/pingGETリクエストを投げるだけのシンプルな仕様です。

表2:PingPong APIリクエストの仕様

URLメソッドヘッダボディ
/api/pingGET特になしGETなのでなし

次にレスポンスメッセージの仕様です。ヘッダについては特に何かしらのものを具体的に指定することはなく、レスポンスボディに以下のリスト10に示すようなフォーマットのJSONを含めるだけです。

リスト10:リスト10:PingPong APIレスポンスボディの仕様

{
    "message": "pong"
}

以降ではテストコードの記述、実装コード本体の記述を進めていきます。作業用に別ブランチ(feature/ping-pong-api)を作成して、そのブランチで作業を進めていきましょう(リスト11)。

リスト11:リスト11:実装用のフィーチャブランチの準備

$ git checkout -b feature/ping-pong-api
PingPong APIのテストコードを記述する

リクエスト、レスポンスの仕様を記述することで、入出力の仕様が明確化されました。では、ロジックの実装の前にテストコードを記述していきましょう。Django REST frameworkのテストの書き方の詳細については、公式ドキュメントを参照してください。今回記述しようとしているPingPong APIのテストコードは、リスト12に示すような形になります。

リスト12:リスト12:server/sampleapp/rest/tests/test_ping.py※2

from django.urls import reverse
from django.test import TestCase
from rest_framework import status
from rest_framework.test import APIClient

class TestPing(TestCase):
    def test_ping_view(self):
        """
        ping-pong APIにリクエストを投げた際に、
        HTTPステータスコードとして200 OK、
        レスポンスボディとして以下のようなJSONを期待する。
        {
            "message": "pong"
        }
        """
        client = APIClient()

        path = reverse('pingpong') # pingpongという名前のついたURLを逆引きする
        response = client.get(path)

        self.assertEqual(response.status_code, status.HTTP_200_OK)
        self.assertEqual(response.data.get('message'), "pong")

※2:パッケージとして認識させるために、当該コードが配置されているディレクトリには__init__.pyという名前をもつファイルの配置を忘れないようにしてください。

もちろんですが、このままだと実装が存在しないので、以下に示すとおりテストは失敗します(リスト13)。

リスト13:リスト13: 実装が存在しないために失敗しているテスト

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
E
======================================================================
ERROR: test_ping_view (rest.tests.test_ping.TestPing)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/fujiwara/repositories/ti_rancher_k8s_sampleapp/server/sampleapp/rest/tests/test_ping.py", line 18, in test_ping_view
    path = reverse('pingpong')
  File "/home/fujiwara/repositories/ti_rancher_k8s_sampleapp/sample/lib/python3.6/site-packages/django/urls/base.py", line 90, in reverse
    return iri_to_uri(resolver._reverse_with_prefix(view, prefix, *args, **kwargs))
  File "/home/fujiwara/repositories/ti_rancher_k8s_sampleapp/sample/lib/python3.6/site-packages/django/urls/resolvers.py", line 622, in _reverse_with_prefix
    raise NoReverseMatch(msg)
django.urls.exceptions.NoReverseMatch: Reverse for 'pingpong' not found. 'pingpong' is not a valid view function or pattern name.

----------------------------------------------------------------------
Ran 1 test in 0.002s

FAILED (errors=1)
Destroying test database for alias 'default'...

これ以降は、このテストを通過させることをゴールとして実装を追加していきましょう。

PingPong APIを実装する

Django REST frameworkを使ったAPI実装の流れは、以下のような流れになります。

  1. シリアライザ(Serializer)を作成する
  2. ビュー(View)を作成する
  3. 作成したビューへのルーティングをurls.pyに記述する

まず、Django REST frameworkにおけるシリアライザですが、複雑なデータをPythonのネイティブオブジェクト(辞書や配列など)に変換したり、さらにそのネイティブオブジェクトをJSONやXMLに変換、またはその逆の処理を助けてくれたりするものだと考えてください。様々な種類のシリアライザが存在するので、詳細については[公式ドキュメントのSerializersのAPIガイド](https://www.django-rest-framework.org/api-guide/serializers/)を参照してください。

今回の場合は、以下のようなメッセージ(リスト10)をPythonのオブジェクトの形式に変換する必要があります。

リスト14:リスト10:PingPong APIレスポンスボディの仕様(再掲)

{
    "message": "pong"
}

では、シリアライザの実装をしてしまいましょう。server/sampleapp/rest/serializers/ping_serializer.pyを下記のとおり作成しました(リスト14)。

リスト15:リスト14:server/sampleapp/rest/serializers/ping_serializer.py

from rest_framework import serializers

class PingSerializer(serializers.Serializer):
    message = serializers.CharField(default='pong')

ここで実装したPingSerializerを使って、PythonオブジェクトをJSONの形式に変換します。シリアライザの実装が終わったので、次はビューの作成に移ります。server/sampleapp/rest/views/ping_view.pyを作成します(リスト15)。

リスト16:リスト15:server/sampleapp/rest/views/ping_view.py

from rest_framework.views import APIView
from rest_framework.response import Response

from rest.serializers.ping_serializer import PingSerializer

class PingView(APIView):
    """
    GETリクエストに対してpongの文言をボディに含んだ
    HTTPレスポンスメッセージを返すAPIです。
    """

    def get(self, request, format=None):
        message = {
            "message": "pong",
        }
        serializer = PingSerializer(message)
        return Response(serializer.data)

PingViewではGETリクエストメッセージを受け取ると、messageキーとその値を含んだ辞書を作成し、PingSerializerでJSON形式に変換してレスポンスを返しています。

最後に/api/pingのURLパスでアクセスがあった場合にリクエストをPingViewに処理させるために、server/sampleapp/rest/urls.pyを設定しましょう(リスト16)。

リスト17:リスト16:server/sampleapp/rest/urls.py

from django.urls import path
from django.contrib import admin

from rest.views.ping_view import PingView

urlpatterns = [
    #/api/pingに来たリクエストをPingViewに流す
    path('ping', PingView.as_view(), name="pingpong"),
]

さて、リクエストのルーティングまで実装が完了したので、再度テストを実行しましょう(リスト17)。

リスト18:リスト17:テストの実行結果

$ python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.010s

OK
Destroying test database for alias 'default'...

ようやくテストをパスすることができました。仕様として予め定めたとおりに実装することができたようです。ここで補足ですが、Django REST frameworkでは、APIの動作確認をするためのUIが存在しています。ここまでで実装したPingPong APIも、ブラウザから動作確認することができます。python manage.py runserverでDjangoの開発用サーバを立ち上げてから、Webブラウザでhttp://127.0.0.1/api/pingにアクセスしてみましょう。正常に動作していれば、図9のような画面が表示されます。

図9:Django REST frameworkのWeb UI画面

図9:Django REST frameworkのWeb UI画面

このようにブラウザからAPIの動作確認を行うことも可能です。しかし、これは実装したAPIが期待した動作をしていない場合の原因特定時など、補助的な利用に留めるようにしましょう。

テストはきちんとコード化して管理することが原則です。

株式会社リクルートテクノロジーズ

ITエンジニアリング本部プロダクティビティエンジニアリング部
クラウドアーキテクトグループ

Rancher JPコミュニティコアメンバー

ユーザ系SIerにてR&Dを経験したのち、2016年より現職。 業務ではパブリッククラウド、コンテナ関連技術を活用した 先進テクノロジーアーキテクチャの事業装着を担当。 Japan Container Days v18.12や、RancherJPのイベント等登壇等複数。

連載バックナンバー

ITインフラ技術解説
第7回

Rancher Pipelineを使ったCI/CD

2019/6/5
連載7回目となる今回は、Rancher Pipelineを用いたCDの実現方法について解説します。
クラウド技術解説
第6回

Rancherのカスタムカタログの作成

2019/5/8
連載6回目となる今回は、作成したアプリケーションをRancherのカタログアプリケーションとして公開する方法を解説する。
クラウド技術解説
第5回

GitLabを用いた継続的インテグレーション

2019/4/25
連載5回目となる今回は、GitLab CIを用いた継続的インテグレーション(CI)について解説します。

Think IT会員サービス無料登録受付中

Think ITでは、より付加価値の高いコンテンツを会員サービスとして提供しています。会員登録を済ませてThink ITのWebサイトにログインすることでさまざまな限定特典を入手できるようになります。

Think IT会員サービスの概要とメリットをチェック

他にもこの記事が読まれています