0
1

LaravelでGitHubのWebhooksを利用して自動化デプロイ

Posted at

はじめに

今回はタイトルにもある通り、LaravelでGitHubのWebhooksという機能を利用してデプロイを自動化する方法をご紹介しようと思います。

処理の流れ:

GitHubのリモートリポジトリで特定のブランチがマージ

GitHubのWebhooksで設定したPayload(URL)にPOSTリクエストがGitHubから送信される

リクエストを受け取った際にシェルスクリプトをバックグラウンドで実行するよう、Laravel側で実装
※ なぜバックグラウンドかは後ほど説明します

ChatWorkなどのSNSツールにエラー通知

それでは早速

GitHub側の設定

まずはGitHub側からpostリクエストを送信するようリポジトリに対して設定しなければいけません。

スクリーンショット 2024-08-04 12.51.03.png

※ 念の為画像も載せておきます。

まずは、GitHubで自動化デプロイを実装したいアプリのリポジトリ画面に移動 -> Settings -> Webhooks -> Settings の画面に移動してください。

Payload URL

Payload URLhttps://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で運用している前提です。

htdocs/deploy.sh
#!/bin/bash

# gitディレクトリのある階層へ移動(ここではhtdocs/../のディレクトリ)
cd ../

# メインブランチからプル
git fetch origin
git pull origin $GIT_BRANCH

それではLaravelで実際に実装

ここではLaravelプロジェクトのエントリーポイントであり、ウェブサーバーがアクセスするディレクトリをhtdocsとして扱っています。

envファイル

まずはLaravelで必要なGitHubのデータを設定ファイル .env に記述しておきます。

htdocs/.env
# webhooks
WEBHOOK_GITHUB_DEPLOY_SECRET=sampleSecret
GIT_BRANCH=master

※ WEBHOOK_GITHUB_DEPLOY_SECRET... GitHubのwebhooksで設定したSecretの値
※ GIT_BRANCH... このブランチがマージされた時にデプロイシェルスクリプトを実行

また、これらの設定値をconfigで参照できるように

htdocs/config/webhook.php
<php

return [
    'github' => [
        'deploy_secret' => env('WEBHOOK_GITHUB_DEPLOY_SECRET', 'null'),
        'deploy_branch' => env('GIT_BRANCH', 'null'),
    ]
]

を作成しておきます。

※ env関数の第二引数であるnullは設定値が定義されていなければnullを返すというものです。

ミドルウェア

誰でもPayload URLにアクセスできるとセキュリティ的によくありませんので、GitHub側で設定したSecretの値を利用してリクエストを検証するミドルウェアを作成します。

htdocs/app/Http/Middleware/Deploy.php
<?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コンテンツを返すことができます。)

例によってミドルウェアの登録をします。

htdocs/app/Http/Kernel.php
protected $routeMiddleware = [
    // 他のミドルウェア...
    'deploy' => \App\Http\Middleware\Deploy::class,
];

ルーティング

htdocs/routes/web.php
Route::post('/webhook/github/deploy', 'DeployController@deploy')
->middleware('Deploy')
htdocs/app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
    // Deploy
    '/webhook/github/deploy'
]

GithubからのリクエストはPOSTリクエストなのでRoute::postとしましょう。

※ また、正直あまり関係ないとは思いますがCsrfトークンの検証をスルーするよう設定しておきました。(私は)

コントローラー

htdocs/app/Http/Controllers/DeployController.php
<?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のブランチを、プロジェクトでメインとなるブランチ(おそらく大体mastermain)と同じものかどうかを判定し、さらにマージされるとシェルスクリプト実行としています。

response()->json([], 200, ['message' => ...としているのは、この記事の前半あたりで述べたように、コントローラーではGitHubにbodyコンテンツを返すことができません。ですのでheader情報として第三引数にデータを入れています。(非推奨でもないが推奨もしません。)

まとめ

やり方は一つではありませんが、あくまで一例として私のやり方を見ていただけると幸いです。
当初はLaravelとGitHubのWebhooksに関する記事が少ないと感じたので、この記事が誰かのお役に立てることを願っています。

おまけ

設定ファイルを変更した際は下記のコマンドを実行してみてください。

php artisan config:clear
php artisan cache:clear
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1