RevComm Tech Blog

コミュニケーションを再発明し 人が人を想う社会を創る

RevComm の Django アプリケーションとチーム開発について紹介します

この記事は RevComm Advent Calendar 2022 の 13 日目の記事です。

はじめに

RevComm の小門です。 普段はインフラエンジニア / SRE としてクラウド (AWS) の全社横断的なセキュリティ強化、統制を推進しています。

また、私自身ここ数年間はサーバサイドエンジニアとして Python、特に Web アプリケーションフレームワークである Django をキャッチアップしています。

RevComm では最近、開発プロジェクトにも関わりはじめました。

RevComm のプロダクトと Django

RevComm の主要プロダクトである MiiTel は営業やコールセンター業務に活用できる IP 電話の SaaS 製品です。
MiiTel の音声解析 AI による分析、可視化した結果を確認するためのダッシュボードである MiiTel Analytics を Web アプリケーションとして開発しています。

MiiTel

この Web アプリケーションはフレームワークに Django を採用しており、本記事ではこのリポジトリについて説明していきます。

プロダクトの初期ローンチ以来、機能拡張や刷新(v1 -> v2)を重ねており、今日まで基本的に 1 つのリポジトリで開発を続けています。
※もちろん、必要に応じて機能分離して別リポジトリ化することもあります。

  • リポジトリの運用開始時期:2019 年ごろ
  • Django アプリケーション数:約 40
  • リポジトリの関係メンバー:約 20 名

このように 3 年以上運用しているプロダクトのリポジトリであり、社内では最も大きなコードベースになっています。

また、ありがたいことに RevComm にはほぼ毎月新しいメンバーが入社してくれていて、オンボーディングが終わり次第、開発プロジェクトにアサインされます。

新規メンバーが大規模なリポジトリの開発でスタートダッシュするために、オンボーディングでのルール周知と「仕組み化」の構築が必要だと思います。
数名程度の規模のチームなら問題にならないかもしませんが、数十名以上のチーム規模の場合に個人の裁量を過度に大きくしてしまうと統制が取れなくなり、将来的に開発スピードが低下する可能性が高くなります。

以下では、大規模なリポジトリで円滑なチーム開発を続けていくために工夫している取り組みと、今後の課題について紹介していきます。

工夫している点

Django は MTV という、一般的な MVC フレームワークに似たアーキテクチャを採用しているフレームワークです。

これについては公式ドキュメントでも言及されています。
FAQ: General | Django documentation | Django

以降は Django の MTV に対応した用語で説明します。

レイヤードアーキテクチャ

一般的な MTV アーキテクチャにおいて、ドメインロジックを実装するのは View とされることが多いです。 多くのケースでは、標準のままの Django でも拡張性と保守性を保ちながら開発していくことが可能だと思います。

しかし、大規模なコードベースにおいて多くの処理が View に集中すると、全体の見通しが悪くなり保守性が低下してしまいます。いわゆる Fat Controller(MTV でいうと Fat View)です。

そこで RevComm では個別のドメインロジックを Usecase 層として View 層から切り離し、View を薄い層として保つようにしています(Usecase 層は View 層と Model 層の間に位置する)。

社内 TechTalk の発表資料より抜粋

ちなみに MiiTel Analytics のプロジェクトは Django に加えて Django REST framework(DRF)も使用しているため UI 層は存在せず、フロントエンドアプリケーションの別リポジトリもあります。

CI/CD の整備

多くのメンバーが関わるリポジトリにおいて運用性と保守性、そして開発利便性の観点から CI/CD を整備することは重要です。
RevComm のリポジトリでは下記のように CI/CD を整備しており、主なツールに GitHub Actions を利用しています。

  • CI
    • Docker コンテナイメージをビルドする
    • ビルドしたコンテナイメージを使ってユニットテストを実行する
  • CD
    • 特定ブランチへのマージを起点に、AWS 環境へのデプロイを実行

GitHub Actions の活用例については過去の記事でも紹介していますので、ぜひご覧ください。
MiiTel Analytics 開発チームの CI / CD ツール活用を紹介します。 - RevComm Tech Blog

エディタ拡張機能の共通化

開発に有用な拡張機能やユーティリティもリポジトリに含めて、チームメンバー間で共有できるようにしています。
(Visual Studio Code を使っているメンバーが多いです)

例を挙げると、API リクエストに便利な拡張機能である REST Client と、その拡張機能で使用する設定ファイル(.http ファイル)をメンバー間で共有しています。

$ tree
.vscode
├── extensions.json
└── settings.json
http
├── my-request.http
├── …

.vscode/extensions.json

{
  "recommendations": [
    "humao.rest-client",
    …
  ]
}

.vscode/extensions.json を活用すると VSCode の拡張機能をプロジェクト内で共通化できて便利です。

課題と対応案

以上はすでに工夫している点でしたが、もちろん現状課題となっている点もあります。 課題点と(採用するかはさておき)取りうる対応案について考えてみます。

アプリケーション起動の複雑性

アプリケーション設定値を環境ごとに使い分けるために環境変数を使用することはよくあると思います。 ところが、大規模な Django ではどうしても必要になる環境変数が増えてきます。

必要な環境変数は .env ファイル化しておけば Docker コンテナ起動時の --env-file オプションで一括で読み込むことができます。

一方で、エディターと連携してデバッガーを起動したり、ユニットテストのためにローカル環境(=非 Docker コンテナ環境)でアプリケーションを起動したりしたいことがあります。

.env ファイルの環境変数を都度 export するのは面倒です。シェルのワンライナーも考えられますが、多くのメンバーがいるチームでの運用は現実的ではありません。

そこで、Docker コンテナの起動オプションと同様に、.env ファイルから動的に環境変数を読み込む方法を考えてみます。
これを実現するには、python-dotenv ライブラリが有用です。
https://github.com/theskumar/python-dotenv

$ pip3 install python-dotenv

settings.py

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent

dotenv_path = BASE_DIR / "path/to/.env"
if dotenv_path.exists():
    from dotenv import load_dotenv
    load_dotenv(dotenv_path, override=True)
…

.env がある場合のみ環境変数を読み込むようにすることで、本番環境では Amazon ECS(コンテナオーケストレーター)でアプリケーション起動する挙動差異を吸収することができます。

テスト実行時間の長さ

大きなコードベースではどうしても CI(特にユニットテスト)の実行時間がネックになってきます。

並列実行

対応策としてまず考えられるのは、テストを並列実行することです。
Django 標準の test コマンド(manage.py test)で並列実行用に --parallel オプションがあり、これが有用そうです。

※なお Django v3 系では -b / --buffer オプションと --parallel オプションは併用ができず、v4 系で可能になるようです。
同僚の松土が Django Congress 2022 で言及していました。資料

マイグレーションのスキップ

また、Django App があるリポジトリをしばらく運用しているとマイグレーション履歴も多くなってきます。
マイグレーションファイルが増えてくると当然マイグレーション実行の所要時間が長くなり、ローカルで何度もテストを実行したい場合に利便性が下がってしまいます。

そのような場合には、Django test コマンドの --keepdb オプションが有用です。

簡単なデモで動作を追ってみます。
1 つの Django App に 1 つのモデル、そして 1 つのテストケースがあります。

$ tree
├── django_project
│   ├── __init__.py
│   ├── settings.py
│   ├── …
├── manage.py
├── myapp
│   ├── __init__.py
│   ├── apps.py
│   ├── migrations
│   ├── models.py
│   ├── tests.py
…

myapp/models.py

from django.db import models

class MyModel(models.Model):
    name = models.CharField("name", max_length=128)

myapp/test.py

from django.test import TestCase

from .models import MyModel

class TestmMyApp(TestCase):
    def test_sample(self):
        MyModel.objects.create(name="hello")
        self.assertEqual(MyModel.objects.count(), 1)

settings.py の中でテスト時にはデータベース名「test-db」を使用するようにしています。

django_project/settings.py

…
DATABASES = {
    'default': {
        "ENGINE": "django.db.backends.postgresql_psycopg2",
        "NAME": "app",
        ...,
        "TEST": {"NAME": "test-db"}
    }
}

--keepdb オプションを指定した場合とそうでない場合、それぞれ 2 回ずつテストを実行して、動作の違いを確認します。

まずは --keepdb がない場合。

$ # 1-a. 1回目
$ python manage.py test -v 2
Found 1 test(s).
Creating test database for alias 'default' ('test-db')...
Operations to perform:
  Apply all migrations: myapp
Running migrations:
  Applying myapp.0001_initial... OK
System check identified no issues (0 silenced).
test_sample (myapp.tests.TestmMyApp) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.025s

OK
Destroying test database for alias 'default' ('test-db')...

$ # 1-b. 2回目
$ python manage.py test -v 2
Found 1 test(s).
Creating test database for alias 'default' ('test-db')...
Operations to perform:
  Apply all migrations: myapp
Running migrations:
  Applying myapp.0001_initial... OK
System check identified no issues (0 silenced).
test_sample (myapp.tests.TestmMyApp) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.025s

OK
Destroying test database for alias 'default' ('test-db')...

1-a、1-b ともに実行ログに Applying myapp.0001_initial... OK とあり、2 回ともマイグレーションが行われていることが分かります。

次に --keepdb を指定する場合。

$ # 2-a. 1回目
$ python manage.py test -v 2 --keepdb
Found 1 test(s).
Using existing test database for alias 'default' ('test-db')...
Operations to perform:
  Apply all migrations: myapp
Running migrations:
  Applying myapp.0001_initial... OK
System check identified no issues (0 silenced).
test_sample (myapp.tests.TestmMyApp) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.010s

OK
Preserving test database for alias 'default' ('test-db')...

$ # 2-b. 2回目
$ python manage.py test -v 2 --keepdb
Found 1 test(s).
Using existing test database for alias 'default' ('test-db')...
Operations to perform:
  Apply all migrations: myapp
Running migrations:
  No migrations to apply.
System check identified no issues (0 silenced).
test_sample (myapp.tests.TestmMyApp) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.009s

OK
Preserving test database for alias 'default' ('test-db')...

ポイントは 2-b 実行結果の中の No migrations to apply. で、2 回目のテストでマイグレーションがスキップされていることが分かります。
一方でテーブルデータはテストケースごとに初期化されるので、何度実行してもテストが通ることも分かります。

大規模なアプリケーションの開発時にローカルで何度もテストを実行したい場合におすすめです。

まとめ

RevComm における 大規模な Django アプリケーションをチームで開発していくために工夫している取り組みについて紹介しました。
同じように Django で開発している方にとって参考になれば幸いです。

上記以外にも多くのノウハウがあり、一方でもちろん課題もあります。
私はプロジェクトに参画してまだ 1 か月程度ですが、改善できる点は積極的に対応して、チーム全体のパフォーマンスを高めていきたいと考えています。

RevComm のプロダクト開発には Python が広く使われており、主要プロダクトではバックエンドに Django を採用しています。
一緒にプロダクトを開発してくれるエンジニアを積極募集中です。

hrmos.co