Laravel & DockerでシンプルなMonoRepo環境を構築する
要約
- LaravelでAPIサーバやバッチが複数あるシステムを構築するとMonolithでもMultiRepoでも開発がつらい。
- Modelとmigrationsディレクトリをパッケージとして分離して使い回し、MonoRepo(一つのリポジトリ)で管理することでつらさを回避しよう。
- 公式のパッケージ開発用ドキュメントを参考にMonoRepoで実用できるレベルに落とし込む。
Monolith, MultiRepoのつらみ
Monolith(一つのプロジェクト & 一つのリポジトリ)
Monolithと言われるとあまりピンとはこないかもしれませんが、composer create-project
してLaravelプロジェクトを作成したデフォルトの構成と言われるとわかりやすいのではないでしょうか。
まず最初にチュートリアルで動かす構成でもありますし、小さなプロジェクトならこれで十分な構成です。
つまりは一つのディレクトリに全てのコードが含まれている状態です。
しかしこの構成の場合、プロジェクトの規模が大きく、ユーザー用と管理用のように明示的にAPIをエンドポイントで分離する場合に不都合が出てきます。
小規模なプロジェクトならばルーティングを/
と/admin
のように分割し一つのサーバーで動かせば済みますが、規模が大きくなると負荷分散やドメインを分けるためにそれぞれ別のサーバーとして分離させたくなります。
Monolithでこの問題を解決する場合、複数のサーバーへ同じコードをデプロイし、ユーザー用サーバーから管理用エンドポイントにアクセスできないように環境変数などを用いて片方のエンドポイントを無効化するなどの方法があげられます。
しかしこの方法だとオペレート時に環境変数の設定ミスにより予期しない環境からアクセスできてしまう危険性があります。
また余分なコードやビルドが発生するためあまり効率が良くなかったり、規模の大きさと比例してコードが複雑化していくのもデメリットです。
MultiRepo(複数のプロジェクト & 複数のリポジトリ)
上記の問題を解決する方法としてMultiRepoという構成があります。
この構成ではユーザー用API、管理用APIとでLaravelプロジェクト自体を複数作成し、それぞれ別のリポジトリで管理します。
そうすることにより別々のサーバーへ独立してデプロイすることができるため、Monolithでのつらみを解消することができます。
しかしLaravelで開発するとなるとまた別の問題が出てきてしまいます。
通常LaravelではModelやmigrationがプロジェクトに依存しています。
そのため分割した全てのプロジェクトで全く同じModelのコードを書かなくてはならず、データベーススキーマに修正が入るたびに全てのリポジトリで更新が必要になります。
リポジトリが2個程度ならまだなんとかなりますが、APIの増加やバッチ処理の追加などにより3個4個と増えていくと指数関数的につらみが増加していきます。[1]
またDDLを管理するmigrationファイルがLaravelプロジェクトに紐づく構造のため、ローカルでの開発やCIの実行時に一つのリポジトリで完結することができず、わざわざ別のデータベーススキーマが定義されているリポジトリを操作する必要があります。
Monolith, MultiRepoからMonoRepoへ
MonoRepoでは複数のプロジェクトを一つのリポジトリに集約します。
Modelやmigrationなどの共通部分をinfraパッケージに抜き出すことによりMultiRepoのつらみを解消しつつも、プロジェクト単位でCI/CDを構築することでMonolithの問題点も解消しています。
ディレクトリ構造としては以下のようになります。
root/
├ user-api/ // Laravel Project
│ ├ app/
│ ├ bootstrap/
│ ├ ...
│ └ composer.json
├ admin-api/ // Laravel Project
│ ├ app/
│ ├ bootstrap/
│ ├ ...
│ └ composer.json
├ infra/ // Package
│ ├ src/
│ ├ database/
│ ├ ...
│ └ composer.json
├ docker/
│ ├ docker-compose.yml
│ ├ user-api/
│ │ └ Dockerfile
│ └ admin-api/
│ └ Dockerfile
└ README.md
このようなディレクトリ構造にすることで各プロジェクトで独立性を保ちながらも、Laravelの規約を崩さずにModelやmigrationなどを共通化することができます。
今回はこの一連の構造を構築する方法を解説していきます。
Laravel Projectの作成
まずは基本となるLaravelプロジェクトを作成します。
user-api
とadmin-api
のどちらでも行う設定は同じので、今回はuser-api
のみに絞って説明していきます。
root/
├ user-api/ // Laravel Project
│ ├ app/
│ ├ bootstrap/
│ ├ ...
│ └ composer.json
└ README.md
Laravelプロジェクトはcomposerでもlaravel/installerでも好きな方法でセットアップしましょう。
ディレクトリの構造上、sailは使用できないので注意してください。
composer create-project laravel/laravel user-api
Dockerの構築
次はdocker
ディレクトリを作成し、Dockerの実行環境を構築します。
root/
├ user-api/ // Laravel Project
│ ├ app/
│ ├ bootstrap/
│ ├ ...
│ └ composer.json
├ docker/
│ ├ docker-compose.yml
│ └ user-api/
│ └ Dockerfile
└ README.md
必要最低限のシンプルな環境です。
FROM php:8.1-apache
COPY /usr/bin/composer /usr/bin/composer
RUN apt-get update && apt-get install -y \
git \
&& docker-php-ext-install pdo_mysql
RUN sed -i 's!/var/www/html!/var/www/app/public!g' /etc/apache2/sites-available/000-default.conf
version: "3.7"
services:
user-api:
build:
context: .
dockerfile: ./user-api/Dockerfile
volumes:
- ../user-api:/var/www/html
environment:
- APP_ENV=local
ports:
- 8080:80
depends_on:
- db
db:
image: mysql:5.7
environment:
- MYSQL_ALLOW_EMPTY_PASSWORD=yes
- MYSQL_DATABASE=my_db
- TZ=Asia/Tokyo
ports:
- 3306:3306
最後にdockerコンテナを立ち上げておきましょう。
cd docker
docker compose up -d
Packageの作成
Laravelの拡張パッケージは公式が専用のドキュメントを用意してくれているので、こちらを参考に作成していきます。
composer.jsonのセットアップ
こちらのページを参考にcomposerのパッケージをセットアップしていきます。
基本的にはデフォルトの設定のままで大丈夫ですが、パッケージの名前はpackages/infra
のようなディレクトリと一致するようなわかりやすい名前にしましょう。
mkdir infra
cd infra
composer init
composer init
を実行すると以下のようなcomposer.json
が作成されます。
{
"name": "packages/infra",
"description": "A demo package",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "John Doe",
"email": "john@doe.com"
}
],
"require": {}
}
また.gitignore
を作成しておくと後々便利です。
/vendor
/composer.lock
/.phpunit.result.cache
必要なライブラリの追加
パッケージで必要なライブラリをcomposer require
で入れます。
テストをしないのであればrequire-devは不要です。
- require
- php
- illuminate/contracts
- illuminate/database
- illuminate/support
- require-dev
- fakerphp/faker
- mockery/mockery
- orchestra/testbench
- phpunit/phpunit
autoloadの設定
パッケージとそれぞれのディレクトリを紐付けます。
// infra/
mkdir src
mkdir tests
{
"name": "packages/infra",
"description": "A demo package",
...
"require-dev": {
"fakerphp/faker": "^1.9.1",
"mockery/mockery": "^1.4.4",
"orchestra/testbench": "^6.23",
"phpunit/phpunit": "^9.5.8"
},
+ "autoload": {
+ "psr-4": {
+ "Sample\\Infra\\": "src/",
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Sample\\Infra\\Tests\\": "tests/"
+ }
+ }
}
Laravel ProjectにPackageを読み込ませる
最低限のパッケージは構成し終えたので一旦user-api
にパッケージを読み込ませてみましょう。
まずはuser-api
からパッケージを参照できるようにdocker-compose.yml
とuser-api
のcomposer.yml
を編集します。
version: "3.7"
services:
user-api:
build:
context: .
dockerfile: ./user-api/Dockerfile
volumes:
- ../user-api:/var/www/html
+ - ../infra:/var/www/html/packages
environment:
- APP_ENV=local
ports:
- 8080:80
depends_on:
- db
...
{
"scripts": { ... },
+ "repositories": [
+ {
+ "type": "path",
+ "url": "packages/infra"
+ }
+ ]
}
最後にdockerコンテナからinfraパッケージを追加します。
cd docker
docker compose exec user-api bash
...
composer require packages/infra
ここまでのディレクトリ構成
最終的に以下のようになっていれば問題ありません。
root/
├ user-api/ // Laravel Project
+├ infra/ // Package
+│ ├ src/
+│ ├ tests/
+│ ├ .gitignore
+│ └ composer.json
├ docker/
└ README.md
ModelとMigrationsのセットアップ
ここからがメインの部分になります。
こちらも公式のドキュメントを参考に作成していきます。
Model
Modelは標準のLaravelでのModelと同じように作成することができます。
src/Models
ディレクトリを作成し、Illuminate\Database\Eloquent\Model
を継承するだけでOKです。
<?php
namespace Sample\Infra\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $guarded = [];
}
namespaceにはcomposer.json
のautoload
で設定した値を指定し、メインのLaravelプロジェクトでもそれを使用して参照することができます。
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use Sample\Infra\Models\Post;
class PostController extends Controller
{
public function find(int $postId): Post
{
return Post::find($postId);
}
}
Migrations
Migrationsに関しても標準のLaravelと作成方法は変わりません。
Migrationファイルの作成
新しくdatabase/migrations
ディレクトリとマイグレーションファイルを作成しましょう。
root/
├ user-api/ // Laravel Project
├ infra/ // Package
│ ├ src/
│ ├ tests/
+│ ├ database/
+│ │ └ migrations/
+│ │ └ 2018_08_08_100000_create_posts_table.php
│ ├ .gitignore
│ └ composer.json
├ docker/
└ README.md
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePostsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('posts');
}
}
ただしphp artisan make:migration
コマンドを使用するとLaravelプロジェクトの方にファイルができてしまうので、コマンドで作成したものを移動させるか自分で一から書きましょう。
その際マイグレーションファイルの命名規則に注意してください。
Migrationの実行
Modelとは異なりマイグレーションファイルを作成しただけではuser-api
プロジェクトの方でphp artisan migrate
を実行してもマイグレーションされません。
なのでPackageにServiceProviderを作成してphp artisan migrate
の対象ファイルとして読み込ませます。
まずはServiceProviderはこちらのページを参考にしてinfra/src
ディレクトリ下に作成しcomposer.json
に追記します。
そしてloadMigrationsFrom
ヘルパを使用することでphp artisan migrate
でinfra/database/migrations
下にある全てのマイグレーションファイルが実行されるようになります(ドキュメント)
<?php
namespace Sample\Infra;
use Illuminate\Support\ServiceProvider;
class InfraServiceProvider extends ServiceProvider
{
public function register()
{
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
}
public function boot()
{
//
}
}
{
...,
"autoload": { ... },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Sample\\Infra\\InfraServiceProvider"
+ ]
+ }
+ }
}
Factory
FactoryやSeederもパッケージ側で設定することが可能です。
infra
下にdatabase/factories
ディレクトリを作成し、そこに標準のLaravelと同様のファクトリクラスを作成します。
<?php
namespace Sample\Infra\Database\Factories;
use Sample\Infra\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;
class PostFactory extends Factory
{
protected $model = Post::class;
public function definition()
{
return [
//
];
}
}
次にcomposer.json
でファクトリへのnamespaceを設定します。
{
...,
"autoload": {
"psr-4": {
"Sample\\Infra\\": "src",
+ "Sample\\Infra\\Database\\Factories\\": "database/factories"
}
},
...
}
設定した後にcomposer dump-autoload
するのを忘れないようにしましょう。
最後にModel側でファクトリの設定を上書きすれば完了です。
class Post extends Model
{
...
+ protected static function newFactory()
+ {
+ return \Sample\Infra\Database\Factories\PostFactory::new();
+ }
}
テスト用の設定
Laravelプロジェクト側でテストを行う場合、Modelが読み込むことができ、php artisan migrate
でパッケージ側のマイグレーションが実行されるため、特に設定など気にする必要なくRefreshDatabase
トレイトなどを使用してコードを書くことができます。
しかしパッケージ内でデータベースに接続するテストを書く場合は一工夫必要となります。
PHPUnitのセットアップ
パッケージ内でテストを行う場合、PHPUnitの設定を追加する必要があります。
こちらもドキュメントに記載されているので順に追っていきましょう。
ますはphpunit
をパッケージに追加し、 phpunit.xml
をinfra
ディレクトリ下に作成します。
APP_KEY
はこのままでも問題ありませんが、気になるようなら任意の値に変更してください。
cd infra
composer require --dev phpunit/phpunit
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
verbose="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
>
<coverage>
<include>
<directory suffix=".php">src/</directory>
</include>
</coverage>
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<php>
<env name="DB_CONNECTION" value="testing"/>
<env name="APP_KEY" value="base64:2fl+Ktvkfl+Fuz4Qp/A75G2RTiWVA/ZoKZvp6fiiM10="/>
</php>
</phpunit>
次にinfra/tests
の下にFeature
ディレクトリとUnit
ディレクトリ、TestCase.php
を作成します。
root/
├ user-api/ // Laravel Project
├ infra/ // Package
│ ├ src/
│ ├ tests/
+│ │ ├ Feature/
+│ │ ├ Unit/
+│ │ └ TestCase.php
│ ├ database/
│ ├ .gitignore
│ └ composer.json
├ docker/
└ README.md
<?php
namespace Sample\Infra\Tests;
use Sample\Infra\InfraServiceProvider;
class TestCase extends \Orchestra\Testbench\TestCase
{
public function setUp(): void
{
parent::setUp();
}
protected function getPackageProviders($app)
{
return [
InfraServiceProvider::class,
];
}
protected function getEnvironmentSetUp($app)
{
// perform environment setup
}
}
データベースへの接続
今回は最初に定義したDockerコンテナのMySQLへアクセスします。
TestCase.php
のgetEnvironmentSetUp
メソッドに接続情報を記載するだけでテスト時に該当のデータベースへアクセスできるようになります。
MySQL以外の接続情報はLaravelプロジェクトのconfig/database.php
にあるものを参考にしてください。
protected function getEnvironmentSetUp($app)
{
$app['config']->set('database.default', 'testbench');
$app['config']->set('database.connections.testbench', [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
]);
}
<php>
<env name="DB_CONNECTION" value="testbench"/>
<env name="APP_KEY" value="base64:2fl+Ktvkfl+Fuz4Qp/A75G2RTiWVA/ZoKZvp6fiiM10="/>
<env name="DB_HOST" value="db"/>
<env name="DB_PORT" value="3306"/>
<env name="DB_DATABASE" value="テスト用データベース名"/>
<env name="DB_USERNAME" value="root"/>
</php>
まとめ
- LaravelでAPIサーバやバッチが複数あるシステムを構築するとMonolithでもMultiRepoでも開発がつらい。
- Modelとmigrationsディレクトリをパッケージとして分離して使い回し、MonoRepo(一つのリポジトリ)で管理することでつらさを回避しよう。
- 公式のパッケージ開発用ドキュメントを参考にMonoRepoで実用できるレベルに落とし込む。
環境構築するだけの内容としてはボリュームが多くなってしまいましたが、コード量自体はそこまで多くなく、リポジトリの数が3,4個以上になるようなプロジェクトでは十分ペイできる内容だと思います。
また最初こそ若干戸惑うかもしれませんがLaravelの規約からはほとんど外れていませんし、公式のパッケージ開発方法をベースにしている[2]ので学習コストもほぼ0に近いのでおすすめです。
Discussion