はじめに
今回はタイトルにもある通り、LaravelでGitHubのWebhooksという機能を利用してデプロイを自動化する方法をご紹介しようと思います。
処理の流れ:
GitHubのリモートリポジトリで特定のブランチがマージ
↓
GitHubのWebhooksで設定したPayload(URL)にPOSTリクエストがGitHubから送信される
↓
リクエストを受け取った際にシェルスクリプトをバックグラウンドで実行するよう、Laravel側で実装
※ なぜバックグラウンドかは後ほど説明します
↓
ChatWorkなどのSNSツールにエラー通知
それでは早速
GitHub側の設定
まずはGitHub側からpostリクエストを送信するようリポジトリに対して設定しなければいけません。
※ 念の為画像も載せておきます。
まずは、GitHubで自動化デプロイを実装したいアプリのリポジトリ画面に移動 -> Settings -> Webhooks -> Settings の画面に移動してください。
Payload URL
Payload URL
にhttps://sample.com/webhook/github/deploy/
と設定しておきましょう。
※ Payload URL
は各自のアプリに従って決めてください。
Content type
application/json
を選択
Secret
sampleSecret
と設定
SSL verification
Enable SSL verification
を選択
Which events would you like to trigger this webhook?
Let me select individual events.
を選択し、Pull requests
を選択。
Active
チェックを入れておいてください。
これでGitHub側の設定は終了です。設定は開発しながら決定していく内容もありますが、今回はスムーズに実装を進めるため最初に全て設定しておきました。
デプロイシェルスクリプト
EC2などサーバーで、プロジェクトのファイルをgitで運用している前提です。
#!/bin/bash
# gitディレクトリのある階層へ移動(ここではhtdocs/../のディレクトリ)
cd ../
# メインブランチからプル
git fetch origin
git pull origin $GIT_BRANCH
それではLaravelで実際に実装
ここではLaravelプロジェクトのエントリーポイントであり、ウェブサーバーがアクセスするディレクトリをhtdocs
として扱っています。
envファイル
まずはLaravelで必要なGitHubのデータを設定ファイル .env
に記述しておきます。
# webhooks
WEBHOOK_GITHUB_DEPLOY_SECRET=sampleSecret
GIT_BRANCH=master
※ WEBHOOK_GITHUB_DEPLOY_SECRET... GitHubのwebhooksで設定したSecret
の値
※ GIT_BRANCH... このブランチがマージされた時にデプロイシェルスクリプトを実行
また、これらの設定値をconfig
で参照できるように
<php
return [
'github' => [
'deploy_secret' => env('WEBHOOK_GITHUB_DEPLOY_SECRET', 'null'),
'deploy_branch' => env('GIT_BRANCH', 'null'),
]
]
を作成しておきます。
※ env関数の第二引数であるnull
は設定値が定義されていなければnull
を返すというものです。
ミドルウェア
誰でもPayload URL
にアクセスできるとセキュリティ的によくありませんので、GitHub側で設定したSecret
の値を利用してリクエストを検証するミドルウェアを作成します。
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class Deploy
{
/**
* Webhookからのリクエストを検証
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$secret = config('webhook.github.deploy_secret');
// リクエストヘッダーから署名を取得
$signature = $request->header('X-Hub-Signature-256');
// 署名が存在しない場合
if (!$signature) {
return response()->json(['message' => 'Signature not provided'], 403);
}
// ハッシュを計算
$hash = 'sha256=' . hash_hmac('sha256', $request->getContent(), $secret);
// 署名が一致しない場合
if (!hash_equals($hash, $signature)) {
return response()->json(['message' => 'Invalid signature'], 403);
}
return $next($request);
}
}
※ コントローラーでは、response()->json(['message' => ...
として、body
コンテンツを指定する第一引数に何を入れても、GitHubに送ることはできませんので、ステータスコードのみを指定しても構いません。(ミドルウェアではGitHubにbody
コンテンツを返すことができます。)
例によってミドルウェアの登録をします。
protected $routeMiddleware = [
// 他のミドルウェア...
'deploy' => \App\Http\Middleware\Deploy::class,
];
ルーティング
Route::post('/webhook/github/deploy', 'DeployController@deploy')
->middleware('Deploy')
protected $except = [
// Deploy
'/webhook/github/deploy'
]
GithubからのリクエストはPOSTリクエストなのでRoute::post
としましょう。
※ また、正直あまり関係ないとは思いますがCsrfトークンの検証をスルーするよう設定しておきました。(私は)
コントローラー
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Exception;
class DeployController extends Controller
{
public function deploy(Request $request)
{
// GitHub Webhookのペイロードを取得し、扱えるデータ形式に変換
$requestBodyJson = $request->getContent();
$requestBodyArray = json_decode($requestBodyJson, true);
// プルリクエストの情報を取得
$baseRef = $requestBodyArray['pull_request']['base']['ref'] ?? null;
$isMerged = $requestBodyArray['pull_request']['merged'] ?? false;
// baseのブランチがmergeされた場合
if ($baseRef === config('webhook.github.deploy_branch') && $isMerged) {
try {
// 実行するシェルスクリプトのパス
$scriptPath = base_path('./deploy.sh');
// シェルスクリプトをバックグラウンドで実行
exec("bash $scriptPath > /dev/null 2>&1 &");
// シェルスクリプトが実行された場合
return response()->json([], 200, ['message' => 'Deployment executed', 'scriptPath' => $scriptPath]);
} catch (Exception $e) {
Log::error($e->getMessage());
report($e);
return response()->json([], 500, ['message' => 'Deployment failed', 'ErrorMessage' => $e->getMessage()]);
}
// baseのブランチがmergeされた場合以外は無視
} else {
return response()->json([], 204, ['message' => "Pull request is not merged into $baseRef branch, deploy process was skipped"]);
}
}
}
GitHubはPOSTリクエストを送信して10秒以上
応答がなければタイムアウトして500系が返されてしまいます。シェルスクリプトの実行完了まで10秒以上かかる場合はバックグラウンドでシェルスクリプトを実行するようにしてください。
※ baseのブランチとは、アクションが起こったブランチです。そのbaseのブランチを、プロジェクトでメインとなるブランチ(おそらく大体master
やmain
)と同じものかどうかを判定し、さらにマージされるとシェルスクリプト実行としています。
※ response()->json([], 200, ['message' => ...
としているのは、この記事の前半あたりで述べたように、コントローラーではGitHubにbody
コンテンツを返すことができません。ですのでheader
情報として第三引数にデータを入れています。(非推奨でもないが推奨もしません。)
まとめ
やり方は一つではありませんが、あくまで一例として私のやり方を見ていただけると幸いです。
当初はLaravelとGitHubのWebhooksに関する記事が少ないと感じたので、この記事が誰かのお役に立てることを願っています。
おまけ
設定ファイルを変更した際は下記のコマンドを実行してみてください。
php artisan config:clear
php artisan cache:clear