この記事では、Django プロジェクトの継続的インテグレーション パイプラインをセットアップする方法を取り上げます (他の Python プロジェクトにも応用可能です)。 Django は、”締め切りに追われる完璧主義者のための Web フレームワーク” がキャッチフレーズの Python フレームワークです。 データベース付きアプリケーションのセットアップやテストの実行が簡単に行えるため、実用最小限の製品 (MVP: Minimal Viable Product) の作成に重宝されています。 また、高品質のコードのデリバリーが可能で、優れたドキュメントが揃っています。 新機能を素早くリリースできようになるので、アプリケーションのユーザーにもメリットがあります。

本記事では、以下の手順に沿って説明を進めます。

前提条件

このチュートリアルを進めるには、以下のインストールが必要です。

Django アプリを作成する

Django フレームワークについては、Django Girls という団体から優れたチュートリアルが提供されています。 この記事では、Django Girls のチュートリアルで作成するブログアプリを使用し、このアプリ用に CircleCI をセットアップします。 データベースには、フラットファイル .sqlite を使用します。

まず、ターミナルで以下のコマンドを入力してリポジトリをクローンし、ブログアプリを取得します。

git clone https://github.com/NdagiStanley/django_girls_complete.git

次に、以下のコマンドでディレクトリを移動します。

cd django_girls_complete

アプリのコードベースについて、私の方で Django Girls のチュートリアルを完了した後に変更を加えています。 この変更は、このプロジェクト用の CI のセットアップに直接関係するものではないため、GitHub のファイルの変更箇所へのリンクを示すに留めます。 変更点は次のとおりです。

  • blog/templates/blog ディレクトリ内にあるテンプレートおよび settings.py更新
  • .gitignore ファイルの追加
  • .editorconfig ファイルの追加
  • アプリを Docker 化するためのファイルの追加 (詳しくは後で説明します。)

元のコードベースを確認するには、以下のコマンドを実行します。

git checkout original

変更後のコードベースを取得するには、以下のコマンドを実行します。

git checkout 1.1.0

それでは、継続的インテグレーション パイプラインのセットアップについて順を追って説明しています。 アプリのフォルダー構造は以下のようになっています (ターミナルで tree を実行すると取得できます)。

.
├── Dockerfile
├── LICENSE
├── README.md
├── blog
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── forms.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── models.py
│   ├── static
│   │   └── css
│   │       └── blog.css
│   ├── templates
│   │   └── blog
│   │       ├── base.html
│   │       ├── post_detail.html
│   │       ├── post_edit.html
│   │       └── post_list.html
│   ├── tests.py
│   ├── urls.py
│   └── views.py
├── docker-compose.yml
├── init.sh
├── manage.py
├── mysite
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── requirements.txt

7 directories, 26 files

アプリのテストを作成する

新しいコミットのマージ前に自動ビルドを行っても問題ないかを確認できるように、CI パイプラインにテストを用意する必要があります。 Django を使ったテストの作成方法については、こちらに詳しいドキュメントがあります。

まず、blog/tests.py のコードを以下のコードで置き換えます。

from django.contrib.auth.models import User
from django.test import TestCase
from django.utils import timezone

from .models import Post
from .forms import PostForm

class PostTestCase(TestCase):
    def setUp(self):
        self.user1 = User.objects.create_user(username="admin")
        Post.objects.create(author=self.user1,
                            title="Test",
                            text="We are testing this",
                            created_date=timezone.now(),
                            published_date=timezone.now())

    def test_post_is_posted(self):
        """Posts are created"""
        post1 = Post.objects.get(title="Test")
        self.assertEqual(post1.text, "We are testing this")

    def test_valid_form_data(self):
        form = PostForm({
            'title': "Just testing",
            'text': "Repeated tests make the app foul-proof",
        })
        self.assertTrue(form.is_valid())
        post1 = form.save(commit=False)
        post1.author = self.user1
        post1.save()
        self.assertEqual(post1.title, "Just testing")
        self.assertEqual(post1.text, "Repeated tests make the app foul-proof")

    def test_blank_form_data(self):
        form = PostForm({})
        self.assertFalse(form.is_valid())
        self.assertEqual(form.errors, {
            'title': ['This field is required.'],
            'text': ['This field is required.'],
        })

ここでは、django.test.TestCase を拡張した PostTestCase クラスを追加しました。このクラスにより、以下の 4 つのメソッドを定義しています。

  • setUp メソッド: def setUp(self) として定義しています。ユーザー 1 人 (self.user1) と、そのユーザーによる投稿を作成します。
  • test_post_is_posted メソッド: Test というタイトルの投稿のテキストが We are testing this であるか検証します。
  • test_valid_form_data メソッド: フォームが適切に保存されることを確認します。具体的には、投稿作成用フォームにタイトルとテキストが入力されていること、投稿が保存されたこと、その投稿のタイトルとテキストが正しいことを検証します。
  • test_blank_form_data メソッド: タイトルもテキストも入力されていない場合にフォームでエラーが返されることを確認します。

次に、以下のコマンドを実行します。

python manage.py test

注: このコマンドは、名前が test で始まるすべてのファイルの TestCase を拡張して作られた全テストケースからテストスイートを作成し、そのテストスイートを実行します。

テスト成功

テストが成功しました。 ひと呼吸しましょう。

テスト追加後のコードベースで作業するために、以下のコマンドを実行します。

git checkout tests

アプリを Docker 化する

次は、アプリを Docker 化します。 作業に移る前に、”アプリの Docker 化” について解説しておきましょう。

アプリの Docker 化とは、Docker を使用してコンテナ内でアプリを開発、デプロイ、実行できるようにすることを言います。 そのために重要なファイルが 3 つあります。

  • .dockerignore ファイル: Git の .gitignore ファイルの Docker 版です。 このファイルに記載したファイルやフォルダーは Docker のコンテキストでは無視され、Docker イメージから除外されます。 このアプリの .dockerignore ファイルはこちらで見ることができます。

  • Dockerfile ファイル: Docker イメージの構築ステップを定義します。 このアプリの Dockerfileこちらで見ることができます。

  • docker-compose.yml ファイル: Docker Compose を使用すると、実行するサービスの数にかかわらず、docker-compose up という 1 行のコマンドを実行するだけで、このファイルのコンテキストでコンテナをスピンアップできます。docker run コマンドを長々と入力する必要はありません。 このアプリの docker-compose.yml ファイルはこちらで見ることができます。

Dockerfile を実行する際に使用する初期化スクリプトも追加しています。 このアプリの init.sh ファイルはこちらで見ることができます。

CircleCI の設定ファイル

CircelCI をプロジェクトと連携するには、Python アプリ用の設定ファイルを追加する必要があります。 プロジェクトのルートに .circleci フォルダーを作成して、config.yml ファイルを追加します。 以下のコードを貼り付けます。

version: 2
jobs:
  build:
    docker:
      - image: circleci/python:3.6
    steps:
      - checkout
      - restore_cache:
          key: deps1-{{ .Branch }}-{{ checksum "requirements.txt" }}
      - run:
          command: |
            python3 -m venv venv
            . venv/bin/activate
            pip install -r requirements.txt
      - save_cache:
          key: deps1-{{ .Branch }}-{{ checksum "requirements.txt" }}
          paths:
            - "venv"
      - run:
          name: テストの実行
          command: |
            . venv/bin/activate
            python3 manage.py test
      - store_artifacts:
          path: test-reports/
          destination: python_app

このファイルでは 4 つのステップを実行しますが、初めての方にはどのような処理が行われるのかわかりにくいと思われるので、それぞれについて以下で説明します。

  • checkout: SSH 経由でソースコードをフェッチし、設定済みのパス (デフォルトでは作業ディレクトリ) にチェックアウトします。
  • restore_cache: 以前保存したキャッシュを復元します。
  • save_cache: ファイルやフォルダーのキャッシュを生成して保存します。 この例の場合、pip install … の実行後に取得、インストールされた Python パッケージのキャッシュを保存します。
  • store_artifacts: ログやバイナリなどを保存し、今後の実行でアプリからアクセスできるようにします。

ローカルで CircleCI ビルドを実行する

コードを GitHub にプッシュして CircleCI で実行する前に、CircleCI CLI ツールをインストールして、ローカルで実行することをお勧めします。 ローカルでビルドを実行すれば、オンラインリポジトリにコードをコミットしなくても、ビルドが成功するかどうかを確認できます。 こうすることで、開発サイクルをスピードアップできます。

Homebrew を使用して以下のコードを実行し、CLI をインストールします。

brew install circleci

次に、circleci switch コマンドを実行して起動します。

circleci switch

その後、circleci config validate を実行して設定ファイルが正しく記述されていることを確認し、circleci build を実行してアプリをビルドします。

circleci config validate

circleci build

このコマンドの出力の末尾が “Success!” であれば、ローカルビルドは正常に実行されています。 チェックが完了したら、コードを GitHub にプッシュしましょう。

CircleCI 設定ファイルを追加した後のコードベースで作業するために、以下のコマンドを実行します。

git checkout circleci

プロジェクトと CircleCI を連携させる

次は、CircleCI と GitHub 上のコードを連携して、CI パイプラインにより魔法を起こしましょう。つまり、コード変更をプッシュすればテストが実行され、そしてテストに成功すればマージが行われるようにします。 ブラウザーで GitHub を開き、新しいリポジトリを作成します。 GitHub アカウントをお持ちでない場合は、こちらで作成してください。 リポジトリを作成したら、GitHub にプロジェクトをプッシュ(英語)します。

その後、CircleCI にログインして、ダッシュボードを表示します。 CircleCI アカウントをお持ちでない場合は、こちらから無料で作成できます。 ダッシュボードページで、[Set Up Project (プロジェクトの設定)] をクリックした後、使用するプロジェクト名をクリックします。 今回は、django_girls_complete です。

[Set Up Project (プロジェクトをセットアップ)]

次に、[Start Building (ビルドを開始)] をクリックします。

ビルドの開始

使用する設定ファイルの選択を求められます。設定ファイルは用意済みなので、[Add Manually (手動で追加)] をクリックしてジョブを実行します。

Add manuallyをクリック

実行に成功すると、以下のように表示されます。

実行に成功

これですべての手順が完了しました。 Django プロジェクトの CI をセットアップする方法は以上となります。 以下のコマンドをターミナルに入力して、アプリを実行しましょう。

docker compose up

アプリが <0.0.0.0:8000> で起動します。

バッジを追加する

コードベースでは、連携機能を複数使用することがよくあります。 他のユーザーにそれらの連携サービスのステータスがわかるよう、README にバッジを含めることをお勧めします。 バッジにより、他のユーザーにサービスのステータスを周知できます。 バッジを取得するには、https://circleci.com/gh/<Username>/<Project>/edit#badges に移動します。 今回は、https://circleci.com/gh/NdagiStanley/django_girls_complete/edit#badges です。

プロジェクト名の横の歯車アイコンをクリックします。

歯車アイコンをクリック

サイドメニューの [Status Badges (ステータスバッジ)] をクリックします。

ステータスバッジ

埋め込みコードをコピーします。

埋め込みコード

コピーしたコードを README に貼り付けます。貼り付ける場所は、上部にあるリポジトリの概要説明の後がお勧めです。 このように README を編集することで、デフォルトブランチで直近に実行されたジョブの[ビルドステータス][11]を把握できるようになります。

キャッシュ機能を試す

ローカルでの CircleCI CLI 実行について説明した際のスクリーンショットに、Error: Skipping cache - error checking storage: not supported という赤いテキストが表示されていたことにお気づきでしょうか。 これは、ローカルでの CircleCI ビルド実行ではキャッシュがサポートされていないことが原因です。

CircleCI では、ジョブの実行時間 (設定ファイル内のコマンドを 1 回実行する時間) を短くするためのキャッシュ機能が実装されています。 この機能の詳細については、こちらをご覧ください。 今回のセットアップでは、キャッシュを行うステップを設定していました。 キャッシュのメリットがわかりやすいように、CircleCI ジョブをキャッシュあり、キャッシュなしで実行してみました。 以下の結果をご覧ください。

キャッシュの例

一番下のビルドから見ていくと、右側に表示されているジョブの実行時間はそれぞれ 11 秒 (00:11)、7 秒などとなっています。

requirements.txt ファイルを変更しない場合、キャッシュ機能を使用すれば pip install -r requirements.txt コマンドを無視することができるので有益です。 1 回目のジョブでは当然ながらキャッシュをセットアップするので、 実行に 11 秒かかったのも不思議ではありません。 2 回目のジョブの結果から、requirements.txt ファイルを変更せずに GitHub に連続してコミットする場合、実行時間は約 7 秒になると考えられます。 3 回目のジョブはキャッシュなしで実行しました。実行時間は最初と同じ、およそ 11 秒に戻りました。 4 回目はキャッシュを再度有効にして実行したところ、20 秒かかりました。 最後に同じジョブを再実行したところ、キャッシュが効果を発揮して、実行時間は 7 秒でした。

ここで短縮できた時間はほんの数秒で、取るに足らないものに思えるかもしれません。しかし、大規模なプロジェクトでは、それが分単位、時間単位の節約につながります。

おわりに

この記事のチュートリアルを終えれば、Django プロジェクトの継続的インテグレーションのマスターまでもう少しです。 本稿では、Django アプリを作成し、テストを記述した後、 独立したコンテナ内でビルドできるようにアプリを Docker 化しました。 Docker 化のメリットは、どのマシンでアプリを実行する場合でも、アプリの実行に必要な依存関係が 1 つ (Docker) だけになることです。 それ以外のアプリの依存関係はすべて、Docker コンテナにインストールされています。 記事ではさらに、CircleCI の使用に関する以下のベストプラクティスも取り上げました。

  • CircleCI CLI を使ってローカルで CI ビルドを実行する
  • バッジを追加する
  • キャッシュを活用してビルドを高速化する

ソフトウェアエンジニアであり、テクニカルコピーライターでもあるスタンリーは、技術チームのリーダーシップやコミュニティへの参加など、さまざまな役割を担ってきました。 彼は自分のことをdigerati(デジタル空間のリテラシー)と言っています。

さんの他の投稿を読む Stanley Ndagi