0. はじめに
大阪のLaravel初学者サウナーこと、kazumakishimoto(@kazuma_dev)です!
LaravelをCircleCI
で自動テスト → Heroku
に自動デプロイする方法です!
0-1. 全体の流れ
0-2. 本記事の対象者
- CircleCIでHerokuに自動テスト自動デプロイしたい方
0-3. 事前準備
- Heroku CLIインストール / Herokuログイン済み
- Heroku手動デプロイ済み
- CircleCIとGitHub連携済み
0-4. 要件
- Heroku設定
- CircleCI自動デプロイ設定
- MySQLやnginxを使う方法もありますが、Herokuでは非推奨
1. Heroku
1-1. 外部API
- HerokuのURLをリダイレクトURIに追加
https://(Herokuのアプリケーション名).herokuapp.com/login/google/callback
1-2. 環境変数
- Config Varsに.env反映
.env
// 略
# Sendgrid
MAIL_DRIVER=smtp
MAIL_HOST=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USERNAME=apikey
MAIL_PASSWORD=SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
MAIL_ENCRYPTION=tls
MAIL_FROM_NAME=memo
MAIL_FROM_ADDRESS=(Sendgridに設定したメールアドレス)
# Google
GOOGLE_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com
GOOGLE_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxx
1-3. composer.json
- Herokuの最新バージョン(heroku-20)がPHP7.2系をサポートしておらず、PHP8がインストールされエラーになります。
ERROR: Dependency installation failed! The 'composer install' process failed with an error.
- composer.json編集
composer.json
"require": {
- "php": "^7.2.5|^8.0",
+ "php": "^7.3.0,",
local
$ docker-compose exec app composer update
1-4. サブディレクトリ設定
- heroku-buildpack-monorepoを追加
local
$ heroku buildpacks:add -a (アプリ名) https://github.com/lstoll/heroku-buildpack-monorepo
- サブディレクトリのパスをconfigに追加
local
$ heroku config:add APP_BASE=(サブディレクトリパス) -a (アプリ名)
- buildpackの順番入れ替え(※言語のbuildpackより上にする必要がある)
1-5. 強制HTTPS化
環境変数
local
$ heroku config:set APP_ENV=production
AppServiceProvider
app/Providers/AppServiceProvider.php
public function boot()
{
if (\App::environment(['production'])) {
\URL::forceScheme('https');
}
}
ForceHttps
local
$ docker-compose exec app php artisan make:middleware ForceHttps
app/Http/Middleware/ForceHttps.php
public function handle($request, Closure $next)
{
if (\App::environment(['production']) && $_SERVER["HTTP_X_FORWARDED_PROTO"] != 'https') {
return redirect()->secure($request->getRequestUri());
}
return $next($request);
}
Kernel
app/Http/Kernel.php
protected $middleware = [
\App\Http\Middleware\ForceHttps::class, // 追加
];
2. CircleCI
2-1. PHPunit
.env.testing
local
$ cp .env .env.testing
$ docker-compose exec app php artisan key:generate --env=testing
base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
$ vi .env.testing
.env.testing
APP_NAME=(アプリ名)
APP_ENV=testing
+ APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx=
APP_DEBUG=true
APP_URL=http://127.0.0.1
LOG_CHANNEL=daily
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel-test
DB_USERNAME=root
DB_PASSWORD=
# 略
ExampleTest削除
- Unit ディレクトリが無いと CI 実行時エラーになる
local
$ rm ./tests/Feature/ExampleTest.php
$ rm ./tests/Unit/ExampleTest.php
$ touch ./tests/Unit/.gitkeep
ArticleControllerTest.php
local
$ docker-compose exec app php artisan make:test ArticleControllerTest
tests/ArticleControllerTest.php
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class ArticleControllerTest extends TestCase
{
use RefreshDatabase;
public function testIndex()
{
$response = $this->get(route('articles.index'));
$response->assertStatus(200)
->assertViewIs('articles.index');
}
public function testGuestCreate()
{
$response = $this->get(route('articles.create'));
$response->assertRedirect(route('login'));
}
public function testAuthCreate()
{
// テストに必要なUserモデルを「準備」
$user = factory(User::class)->create();
// ログインして記事投稿画面にアクセスすることを「実行」
$response = $this->actingAs($user)
->get(route('articles.create'));
// レスポンスを「検証」
$response->assertStatus(200)
->assertViewIs('articles.create');
}
}
ArticleTest.php
local
$ docker-compose exec app php artisan make:test ArticleTest
tests/ArticleTest.php
<?php
namespace Tests\Feature;
use App\Models\Article;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class ArticleTest extends TestCase
{
use RefreshDatabase;
public function testIsLikedByNull()
{
$article = factory(Article::class)->create();
$result = $article->isLikedBy(null);
$this->assertFalse($result);
}
public function testIsLikedByTheUser()
{
$article = factory(Article::class)->create();
$user = factory(User::class)->create();
$article->likes()->attach($user);
$result = $article->isLikedBy($user);
$this->assertTrue($result);
}
public function testIsLikedByAnother()
{
$article = factory(Article::class)->create();
$user = factory(User::class)->create();
$another = factory(User::class)->create();
$article->likes()->attach($another);
$result = $article->isLikedBy($user);
$this->assertFalse($result);
}
}
ArticleFactory.php
local
$ docker-compose exec app php artisan make:factory ArticleFactory --model=Article
database/factories/ArticleFactory.php
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use App\Models\Article;
use App\Models\User;
use Faker\Generator as Faker;
$factory->define(Article::class, function (Faker $faker) {
return [
'title' => $faker->text(50),
'body' => $faker->text(500),
'user_id' => function () {
return factory(User::class);
}
];
});
2-2. CircleCI
database.php
config/database.php
'circle_testing' => [
'driver' => 'mysql',
'host' => '127.0.0.1',
'port' => '3306',
'database' => 'circle_test',
'username' => 'root',
'password' => '',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
],
config.yml
local
$ mkdir .circleci && touch .circleci/config.yml
.circleci/config.yml
version: 2.1
executors:
laravel-circleci:
docker:
- image: circleci/php:7.4-node-browsers
- image: circleci/mysql:5.7
environment:
- APP_DEBUG: true
- APP_ENV: testing
- APP_KEY: base64:oQbVvWs3tHsouVVGIhue/ZQX3p7OuQ8Z4s6oDOhXK7I=
- DB_CONNECTION: circle_testing
- MYSQL_ALLOW_EMPTY_PASSWORD: true
working_directory: ~/repo
commands:
install-dockerize:
steps:
- run:
name: Install dockerize
command: wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
environment:
DOCKERIZE_VERSION: v0.6.1
install-php-extensions:
steps:
- run:
name: Install PHP Exetensions
command: sudo docker-php-ext-install pdo_mysql
working_directory: src
restore-cache-composer:
steps:
- restore_cache:
key: v1-dependencies-{{ checksum "src/composer.json" }}
install-composer:
steps:
- run:
name: Install Composer
command: composer install -n --prefer-dist
working_directory: src
save-cache-composer:
steps:
- save_cache:
key: v1-dependencies-{{ checksum "src/composer.json" }}
paths:
- vendor
npm-ci:
steps:
- run:
name: npm CI
command: |
if [ ! -d node_modules ]; then
npm ci
fi
working_directory: src
restore-cache-npm:
steps:
- restore_cache:
key: npm-cache-{{ checksum "src/package-lock.json" }}
npm-run-dev:
steps:
- run:
name: Run npm
command: npm run dev
working_directory: src
save-cache-npm:
steps:
- save_cache:
key: npm-cache-{{ checksum "src/package-lock.json" }}
paths:
- node_modules
migration-seeding:
steps:
- run:
name: Migration & Seeding
command: php artisan migrate --seed
working_directory: src
test-unittest:
steps:
- run:
name: Run PHPUnit
command: vendor/bin/phpunit
working_directory: src
jobs:
build:
executor:
name: laravel-circleci
steps:
- checkout
- install-dockerize
- install-php-extensions
- restore-cache-composer
- install-composer
- save-cache-composer
- restore-cache-npm
- npm-ci
- save-cache-npm
- npm-run-dev
- migration-seeding
- test-unittest
deploy:
docker:
- image: circleci/php:7.4-node-browsers
steps:
- checkout
- run:
name: heroku deploy
command: |
git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git master
workflows:
version: 2
build_deploy:
jobs:
- build
- deploy:
requires:
- build
filters:
branches:
only:
- master
2-3. CircleCIでMySQLを使用する
phpunit.xml
# 略
<php>
<server name="APP_ENV" value="testing"/>
<server name="BCRYPT_ROUNDS" value="4"/>
<server name="CACHE_DRIVER" value="array"/>
- <server name="DB_CONNECTION" value="sqlite"/>
- <server name="DB_DATABASE" value=":memory:"/>
+ <server name="DB_CONNECTION" value="mysql"/>
+ <server name="DB_DATABASE" value="laravel-test"/>
<server name="MAIL_DRIVER" value="array"/>
<server name="QUEUE_CONNECTION" value="sync"/>
<server name="SESSION_DRIVER" value="array"/>
</php>
</phpunit>
2-4. テスト失敗時にmasterブランチへマージ不可にする
2-5. 環境変数
2-6. 動作確認
3. 補足
3-1. 開発環境(FW/ツールのバージョンなど)
ツール | バージョン |
---|---|
Vue.js | 2.6.14 |
jQuery | 3.4.1 |
PHP | 7.4.1 |
Laravel | 6.20.43 |
MySQL | 5.7.36 |
Nginx | 1.18.0 |
Composer | 2.0.14 |
npm | 6.14.6 |
Git | 2.33.1 |
Docker | 20.10.11 |
docker-compose | v2.2.1 |
PHPUnit | 8.0 |
CircleCI | 2.1 |
heroku | 7.59.4 |
MacBook Air | M1,2020 |
macOS | Monterey 12.3 |
Homebrew | 3.3.8 |
3-2. ディレクトリ構造
【ルートディレクトリ】
├─ .circleci
│ └─ config.yml
├─ aws / CloudFormation
│ └─ ec2.yml
├─ docker
│ └─ mysql
│ └─ nginx
│ └─ php
│ └─ phpmyadmin
├─ src
│ └─ 【Laravelのパッケージ】
│─ .env
│─ .gitignore
└─ docker-compose.yml
Reference
- Laravel × CircleCI × AWSで学ぶCI/CD | Techpit
- 絶対に失敗しないLaravel+Vue.jsのアプリをHerokuにデプロイする方法〜画像付きで丁寧に説明〜
- 【Heroku】LaravelでHerokuデプロイするとThe 'composer install' process failed with an error.が発生 - Qiita
- Heroku PHP Support | Heroku Dev Center
- HerokuにGitHubリポジトリの一部だけデプロイする - Qiita
- nuxtのサブディレクトリをHerokuにデプロイする方法 - Qiita
- LaravelでURLをHTTPS化させるメモ (Heroku) - Qiita
- LaravelをCircleCI 2.0で自動テストしherokuへ自動デプロイ - Qiita