GitLab CI/CDとEnvoyを使ったLaravelアプリケーションのテストとデプロイ

導入

GitLab は継続的インテグレーションでアプリケーションを機能させ、新しいコード変更をいつでも簡単に本番サーバーにデプロイすることができます。

このチュートリアルでは、Laravelアプリケーションの初期化とEnvoyタスクの設定方法を紹介し、GitLab CI/CDによる 継続的デリバリーでテストとデプロイを行う方法を紹介します。

LaravelとLinuxサーバーの基本的な経験があり、GitLabの使い方を知っていることを前提としています。

LaravelはPHPで書かれた高品質のWebフレームワークです。素晴らしいコミュニティがあり、ドキュメントも充実しています。通常のルーティング、コントローラ、リクエスト、レスポンス、ビュー、(ブレード)テンプレートはもちろんのこと、Laravelはキャッシュ、イベント、ローカライズ、認証など多くの追加サービスを提供しています。

PHPベースのSSHタスクランナーとしてEnvoyを使用します。リポジトリからのプロジェクトのクローン、Composerの依存関係のインストール、Artisanコマンドの実行など、リモートサーバー上で実行できるタスクを設定するために、クリーンで最小限のBlade構文を使用します。

GitLab上でLaravelアプリを初期化します。

新しい Laravel プロジェクトをインストールしたと仮定して、まずはユニットテストを行い、プロジェクトの Git を初期化しましょう。

ユニットテスト

Laravelの新規インストール(現在8.0)には、testsディレクトリに’Feature’と’Unit’の2種類のテストが含まれています。test/Unit/ExampleTest.php からのユニットテストです:

<?php

namespace Tests\Unit;

...

class ExampleTest extends TestCase
{
    public function testBasicTest()
    {
        $this->assertTrue(true);
    }
}

このテストは、与えられた値がtrueであることをアサートするだけのシンプルなものです。

LaravelはデフォルトでテストにPHPUnitvendor/bin/phpunit を実行すると、緑色の出力が表示されるはずです:

vendor/bin/phpunit
OK (1 test, 1 assertions)

このテストは、後でGitLab CI/CDでアプリを継続的にテストするために使用します。

GitLab にプッシュします。

ローカルでアプリを立ち上げることができたので、次はコードベースをリモートリポジトリにプッシュしましょう。GitLab にlaravel-sample という名前の新しいプロジェクトを作成しましょう。プロジェクトのトップページに表示されるコマンドラインにしたがってリポジトリを立ち上げ、最初のコミットをプッシュします。

cd laravel-sample
git init
git remote add origin git@gitlab.example.com:<USERNAME>/laravel-sample.git
git add .
git commit -m 'Initial Commit'
git push -u origin main

本番サーバーの設定

Envoy と GitLab CI/CD の設定を始める前に、本番サーバーがデプロイできる状態になっていることを確認しましょう。Linux, NGINX, MySQL, PHP の略である LEMP スタックを Ubuntu 16.04 にインストールしました。

新しいユーザーを作成します。

それでは、ウェブサイトをデプロイするために使用する新しいユーザーを作成し、Linux ACL を使用して必要な権限を与えてみましょう:

# Create user deployer
sudo adduser deployer
# Give the read-write-execute permissions to deployer user for directory /var/www
sudo setfacl -R -m u:deployer:rwx /var/www

UbuntuサーバーにACLがインストールされていない場合は、このコマンドを使ってインストールしてください:

sudo apt install acl

SSHキーの追加

GitLab 上の非公開リポジトリから本番サーバーにアプリをデプロイしたいとしましょう。まず、デプロイユーザーのためにパスフレーズなしのSSH キーペアを生成する必要があります。

その後、デプロイ作業を自動化するために、デプロイユーザーとしてサーバーに SSH 接続するための秘密鍵をコピーします:

# As the deployer user on server
#
# Copy the content of public key to authorized_keys
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
# Copy the private key text block
cat ~/.ssh/id_rsa

では、これをCI/CD変数としてGitLabプロジェクトに追加しましょう。プロジェクトの CI/CD 変数はユーザー定義の変数で、セキュリティのために.gitlab-ci.yml の外に保存されます。プロジェクトの設定>CI/CDからプロジェクトごとに追加することができます。

KEYフィールドにSSH_PRIVATE_KEY という名前を追加し、VALUEフィールドに先ほどコピーした秘密鍵を貼り付けます。この変数は後で.gitlab-ci.yml 、パスワードを入力せずにデプロイユーザーとしてリモートサーバーに簡単に接続するために使用します。

variables page

また、公開キーをプロジェクト>設定>リポジトリに デプロイキーとして追加する必要があります。これにより、SSHプロトコルを使用してサーバーからリポジトリにアクセスできるようになります。

# As the deployer user on the server
#
# Copy the public key
cat ~/.ssh/id_rsa.pub

Title」フィールドに任意の名前を追加し、「Key」フィールドに公開鍵を貼り付けます。

deploy keys page

では、deployer ユーザーがリポジトリにアクセスできることを確認するために、サーバー上にリポジトリをクローンしてみましょう。

# As the deployer user on server
#
git clone git@gitlab.example.com:<USERNAME>/laravel-sample.git

Are you sure you want to continue connecting (yes/no)? と聞かれたらyes と答えましょう。GitLab.com が既知のホストに追加されます。

NGINXの設定

ここで、ウェブサーバの設定がpublic ではなくcurrent/public を指していることを確認しましょう。

デフォルトのNGINXサーバーブロック設定ファイルを開きます:

sudo nano /etc/nginx/sites-available/default

設定は次のようになります。

server {
    root /var/www/app/current/public;
    server_name example.com;
    # Rest of the configuration
}

/var/www/app/current/public 、アプリ名をアプリのフォルダ名に置き換えてもかまいません。

Envoyのセットアップ

これでLaravelアプリの本番準備は完了です。次はEnvoyを使ってデプロイを行います。

Envoyを使用するには、まずLaravelの指示に従ってローカルマシンにインストールします。

Envoyの仕組み

Envoyの長所は、Bladeエンジンを必要とせず、Bladeのシンタックスを使用してタスクを定義できることです。まず始めに、アプリのルートにEnvoy.blade.php 、Envoyをテストするシンプルなタスクを作成します。

@servers(['web' => 'remote_username@remote_host'])

@task('list', ['on' => 'web'])
    ls -l
@endtask

ご想像の通り、ファイルの先頭の@servers ディレクティブ内に配列があり、web というキーとサーバーのアドレスの値(例えば、deployer@192.168.1.1 )がコンテナとして格納されています。そして、@task ディレクティブの中に、タスクが実行されたときにサーバー上で実行される Bash コマンドを定義します。

内部マシンではrun コマンドを使用して Envoy タスクを実行します。

envoy run list

先ほど定義したlist タスクが実行され、サーバに接続してディレクトリの内容をリストアップします。

EnvoyはLaravelの依存関係ではないので、どのようなPHPアプリケーションにも使用できます。

ダウンタイムなしのデプロイ

本番サーバにデプロイするたびに、Envoy は GitLab リポジトリからアプリの最新リリースをダウンロードし、プレビューリリースに置き換えます。Envoy はダウンタイムなしでこれを行うので、誰かがサイトをレビューしている間、デプロイ中に心配する必要はありません。私たちのデプロイ計画は、GitLab リポジトリから最新リリースをクローンし、Composer の依存関係をインストールし、最後に新しいリリースを有効化することです。

setup ディレクティブ

デプロイプロセスの最初のステップは、@setupディレクティブで変数のセットを定義することです。app はアプリケーションの名前に変えてもかまいません:

...

@setup
    $repository = 'git@gitlab.example.com:<USERNAME>/laravel-sample.git';
    $releases_dir = '/var/www/app/releases';
    $app_dir = '/var/www/app';
    $release = date('YmdHis');
    $new_release_dir = $releases_dir .'/'. $release;
@endsetup

...
  • $repository はリポジトリのアドレスです。
  • $releases_dir ディレクトリはアプリをデプロイする場所です。
  • $app_dir はサーバー上で動作するアプリの実際の場所です。
  • $release アプリの新しいリリースをデプロイするたびに、現在の日付を名前に持つ新しいフォルダを取得します。
  • $new_release_dir は新しいリリースのフルパスです。

storyディレクティブ

storyディレクティブは、1つのタスクとして実行できるタスクのリストを定義することができます。ここでは、clone_repository,run_composer,update_symlinks という3つのタスクがあります。これらの変数は、タスクのコードをよりすっきりさせるために使用します:

...

@story('deploy')
    clone_repository
    run_composer
    update_symlinks
@endstory

...

これら3つのタスクを1つずつ作ってみましょう。

リポジトリのクローン

最初のタスクは、releases ディレクトリを作成し(存在しない場合)、リポジトリのmain ブランチ(デフォルト)を$new_release_dir 変数で指定した新しいリリースディレクトリにクローンします。releases ディレクトリには、すべてのデプロイが格納されます:

...

@task('clone_repository')
    echo 'Cloning repository'
    [ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
    git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
    cd {{ $new_release_dir }}
    git reset --hard {{ $commit }}
@endtask

...

プロジェクトが大きくなるにつれて、Git の履歴も長くなっていきます。リリースごとにディレクトリを作成するので、リリースごとにプロジェクトの履歴をダウンロードする必要はないかもしれません。--depth 1 オプションは、システムの時間とディスクスペースを節約できる素晴らしいソリューションです。

Composerによる依存関係のインストール

ご存知かもしれませんが、このタスクは新しいリリースディレクトリに移動し、Composerを実行してアプリケーションの依存関係をインストールするだけです:

...

@task('run_composer')
    echo "Starting deployment ({{ $release }})"
    cd {{ $new_release_dir }}
    composer install --prefer-dist --no-scripts -q -o
@endtask

...

新しいリリースのアクティベート

新しいリリースの要件を準備したら、次にすることは、ストレージディレクトリを削除し、アプリケーションのstorage ディレクトリと.env ファイルを新しいリリースに向けるためのシンボリックリンクを 2 つ作成することです。次に、current アプリディレクトリに配置した current名前で、新しいリリースへのシンボリックリンクをもう1つ作成する必要があります。current シンボリックリンクは current常にアプリの最新リリースを指します:

...

@task('update_symlinks')
    echo "Linking storage directory"
    rm -rf {{ $new_release_dir }}/storage
    ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/storage

    echo 'Linking .env file'
    ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/.env

    echo 'Linking current release'
    ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current
@endtask

ご覧のように、ln コマンドのオプションとして-nfs を使用しています。これは、storage.envcurrent がプレビューのリリースを指すことがなくなり、強制的に新しいリリースを指すようになることを意味します(-nfs からのf は強制を意味します)。

フルスクリプト

スクリプトは準備できましたが、deployer@192.168.1.1 をあなたのサーバーに変更し、/var/www/app をアプリをデプロイしたいディレクトリに変更してください。

最後に、Envoy.blade.php ファイルは次のようになります:

@servers(['web' => 'deployer@192.168.1.1'])

@setup
    $repository = 'git@gitlab.example.com:<USERNAME>/laravel-sample.git';
    $releases_dir = '/var/www/app/releases';
    $app_dir = '/var/www/app';
    $release = date('YmdHis');
    $new_release_dir = $releases_dir .'/'. $release;
@endsetup

@story('deploy')
    clone_repository
    run_composer
    update_symlinks
@endstory

@task('clone_repository')
    echo 'Cloning repository'
    [ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
    git clone --depth 1 {{ $repository }} {{ $new_release_dir }}
    cd {{ $new_release_dir }}
    git reset --hard {{ $commit }}
@endtask

@task('run_composer')
    echo "Starting deployment ({{ $release }})"
    cd {{ $new_release_dir }}
    composer install --prefer-dist --no-scripts -q -o
@endtask

@task('update_symlinks')
    echo "Linking storage directory"
    rm -rf {{ $new_release_dir }}/storage
    ln -nfs {{ $app_dir }}/storage {{ $new_release_dir }}/storage

    echo 'Linking .env file'
    ln -nfs {{ $app_dir }}/.env {{ $new_release_dir }}/.env

    echo 'Linking current release'
    ln -nfs {{ $new_release_dir }} {{ $app_dir }}/current
@endtask

デプロイする前にもう1つすべきことは、アプリケーションのstorage フォルダを手動でサーバの/var/www/app ディレクトリにコピーすることです。この作業は別のEnvoyタスクで行うことができます。また、Laravelの本番環境変数を設定するために、同じパスに.env 。これらは永続的なデータで、新しいリリースごとに共有されます。

ここで、envoy run deploy を実行してアプリをデプロイする必要がありますが、このチュートリアルの後半で説明する CI の環境で GitLab が処理してくれるので、その必要はありません。

いよいよEnvoy.blade.phpをコミットしてブランチにプッシュしますmainmainコラボレーションはこのチュートリアルの範囲外なのでmain 、シンプルに mainするために、フィーチャーブランチを使わずにmain 直接コミット mainします。実際のプロジェクトでは、ブランチをまたがってコードを移動させるためにイシュートラッカーや マージリクエストを使用します:

git add Envoy.blade.php
git commit -m 'Add Envoy'
git push origin main

GitLab による継続的インテグレーション

GitLab でアプリの準備ができたので、手動でデプロイすることもできます。しかし、一歩進んで継続的デリバリーを使って自動でデプロイしてみましょう。イシューにいち早く気づくために、自動化されたテストのセットですべてのコミットをチェックし、テストの結果に満足したらターゲット環境にデプロイする必要があります。

GitLab CI/CDではDockerエンジンを使ってアプリのテストとデプロイのプロセスを処理することができます。Dockerについてよく知らない場合は、Set up automated buildsを参照してください。

GitLab CI/CDでアプリをビルド、テスト、デプロイできるようにするには、作業環境を準備する必要があります。そのために、Laravelアプリの実行に必要な最低限の要件を備えたDockerイメージを使用します。他にも方法はありますが、ビルドの動作が遅くなる可能性があります。

コンテナイメージの作成

アプリのルート・ディレクトリに、以下の内容でDockerfileを作成しましょう:

# Set the base image for subsequent instructions
FROM php:7.4

# Update packages
RUN apt-get update

# Install PHP and composer dependencies
RUN apt-get install -qq git curl libmcrypt-dev libjpeg-dev libpng-dev libfreetype6-dev libbz2-dev

# Clear out the local repository of retrieved package files
RUN apt-get clean

# Install needed extensions
# Here you can install any other extension that you need during the test and deployment process
RUN docker-php-ext-install mcrypt pdo_mysql zip

# Install Composer
RUN curl --silent --show-error "https://getcomposer.org/installer" | php -- --install-dir=/usr/local/bin --filename=composer

# Install Laravel Envoy
RUN composer global require "laravel/envoy=~1.0"

公式のPHP 7.4 Dockerイメージを追加しました。このイメージは、PHPがプリインストールされたDebian busterの最小インストールで構成されており、私たちのユースケースで完璧に動作します。

必要なPHP拡張モジュールをインストールするために、docker-php-ext-install (公式のPHP Dockerイメージで提供されています)を使用しました。

GitLabコンテナレジストリのセットアップ

Dockerfile ができたので、ビルドしてGitLab コンテナレジストリにプッシュしましょう。

レジストリは、後で使う画像を保存したりタグ付けしたりする場所です。開発者は、非公開のイメージや会社のイメージ、あるいはテストにだけ使う使い捨てのイメージのために、自分自身のレジストリを管理したいと思うかもしれません。GitLab Container Registryを使えば、別のサービスを立ち上げて管理したり、公開レジストリを使う必要はありません。

GitLabプロジェクトのリポジトリでレジストリタブに移動します。

container registry page empty image

このタブを見るには、プロジェクトのコンテナレジストリを有効にする必要があるかもしれません。プロジェクトのSettings > General > Visibility, project features, permissions にあります。

私たちのマシンでContainer Registryを使い始めるには、まずGitLabのユーザー名とパスワードを使ってGitLabレジストリにサインインする必要があります。DockerMachineがインストールされていることを確認してから、以下のコマンドを実行します:

docker login registry.gitlab.com

そしてイメージをビルドしてGitLabにプッシュします:

docker build -t registry.gitlab.com/<USERNAME>/laravel-sample .

docker push registry.gitlab.com/<USERNAME>/laravel-sample

おめでとうございます!ページを更新すると、最初の Docker イメージが GitLab レジストリにプッシュされているはずです:

container registry page with image

GitLab CI/CDを使ってDockerイメージをビルドしてプッシュすることもできます。

このイメージは.gitlab-ci.yml 設定ファイルのさらに下のほうで、アプリのテストとデプロイの処理に使います。

Dockerfile ファイルをコミットしましょう。

git add Dockerfile
git commit -m 'Add Dockerfile'
git push origin main

GitLab CI/CDのセットアップ

GitLab CI/CD でアプリをビルドしてテストするには、リポジトリのルートに.gitlab-ci.yml というファイルが必要です。これは Circle CI や Travis CI と似ていますが、GitLab に組み込まれています。

.gitlab-ci.yml ファイルは以下のようになります:

default:
  image: registry.gitlab.com/<USERNAME>/laravel-sample:latest
  services:
    - mysql:5.7

variables:
  MYSQL_DATABASE: homestead
  MYSQL_ROOT_PASSWORD: secret
  DB_HOST: mysql
  DB_USERNAME: root

stages:
  - test
  - deploy

unit_test:
  stage: test
  script:
    - cp .env.example .env
    - composer install
    - php artisan key:generate
    - php artisan migrate
    - vendor/bin/phpunit

deploy_production:
  stage: deploy
  script:
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - ssh-add <(echo "$SSH_PRIVATE_KEY")
    - mkdir -p ~/.ssh
    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
    - ~/.composer/vendor/bin/envoy run deploy --commit="$CI_COMMIT_SHA"
  environment:
    name: production
    url: http://192.168.1.1
  when: manual
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

これだけでも大変でしょう?それでは、一歩ずつ見ていきましょう。

イメージとサービス

Runner は .gitlab-ci.yml で定義されたスクリプトを実行します。image キーワードは、Runnerに使用する画像を指示します。services キーワードは、メインイメージにリンクされる追加イメージを定義します。ここでは、前に作成したコンテナイメージをメインイメージとして使用し、サービスとして MySQL 5.7 を使用します。

default:
  image: registry.gitlab.com/<USERNAME>/laravel-sample:latest
  services:
    - mysql:5.7

...

異なる PHP バージョンやデータベース管理システムでアプリをテストしたい場合は、テストジョブごとに異なるimageservices キーワードを定義できます。

CI/CD 変数

GitLab CI/CDではCI/CD変数をジョブで使うことができます。データベース管理システムとしてMySQLを定義しました。MySQLにはデフォルトでスーパーユーザーrootが作成されています。

そこで、MYSQL_DATABASE 変数をデータベース名として、MYSQL_ROOT_PASSWORD 変数をroot のパスワードとして定義することで、MySQLインスタンスの設定を調整する必要があります。 MySQL変数についての詳細は、公式のMySQL Docker Imageを参照してください。

また、変数DB_HOSTmysql に、DB_USERNAMEroot に設定します。これらはLaravel固有の変数です。メインDockerイメージにリンクされたサービスとしてMySQL Dockerイメージを使用するため、127.0.0.1 の代わりにmysql としてDB_HOST を定義します。

variables:
  MYSQL_DATABASE: homestead
  MYSQL_ROOT_PASSWORD: secret
  DB_HOST: mysql
  DB_USERNAME: root

最初のジョブとしてのユニットテスト

unit_test ジョブを実行するときに実行されるスクリプトキーワードの配列として、必要な Shell スクリプトを定義しました。

これらのスクリプトはLaravelを準備するためのいくつかのArtisanコマンドで、スクリプトの最後に、PHPUnit によってテストを実行します。

unit_test:
  script:
    # Install app dependencies
    - composer install
    # Set up .env
    - cp .env.example .env
    # Generate an environment key
    - php artisan key:generate
    # Run migrations
    - php artisan migrate
    # Run tests
    - vendor/bin/phpunit

プロダクションへのデプロイ

ジョブdeploy_production はアプリを本番サーバーにデプロイします。Envoyでアプリをデプロイするために、$SSH_PRIVATE_KEY 変数をSSH秘密鍵として設定する必要がありました。SSHキーが正常に追加されたら、Envoyを実行できます。

前述したように、GitLabは継続的デリバリーの手法もサポートしています。environmentキーワードは、このジョブがproduction 環境にデプロイすることをGitLabに伝えます。url キーワードは、GitLab Environments ページに私たちのアプリケーションへのリンクを生成するために使われます。rules:if キーワードは、パイプラインがmain ブランチをビルドしているときだけジョブを実行するように GitLab CI/CD に伝えます。最後に、when: manual はジョブを自動実行から手動アクションに切り替えるために使います。

deploy_production:
  script:
    # Add the private SSH key to the build environment
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
    - eval $(ssh-agent -s)
    - ssh-add <(echo "$SSH_PRIVATE_KEY")
    - mkdir -p ~/.ssh
    - '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
    # Run Envoy
    - ~/.composer/vendor/bin/envoy run deploy
  environment:
    name: production
    url: http://192.168.1.1
  when: manual
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

本番環境にデプロイする前にアプリケーションを最終テストするために、ステージング環境用の別のジョブを追加することもできます。

GitLab CI/CD をオンにします。

GitLab CI/CDでアプリをテストしてデプロイするために必要なものはすべて用意しました。そのためには、.gitlab-ci.yml をコミットしてmain ブランチにプッシュします。これでパイプラインが起動し、プロジェクトのパイプラインからライブで見ることができます。

pipelines page

ここでは、テストと デプロイのステージを見てみましょう。Testステージではunit_test ビルドが実行されています。Runner の出力を見るには、これを選択してください。

pipeline page

コードがパイプラインを正常に通過したら、右側の再生ボタンを選択して本番サーバーにデプロイできます。

pipelines page deploy button

デプロイパイプラインが正常に通過したら、パイプライン > 環境に移動します。

environments page

期待通りに動作しない場合は、アプリの最新動作バージョンにロールバックできます。

environment page

右側にある外部リンクアイコンを選択すると、GitLabが本番用のウェブサイトを開きます。デプロイが成功し、アプリケーションが稼動していることがわかります。

Laravel welcome page

デプロイ後の本番サーバー上のアプリケーションのディレクトリ構造がどうなっているのか知りたい場合は、currentreleasesstorage という名前の3つのディレクトリをご覧ください。ご存知のように、current ディレクトリは最新リリースを指すシンボリックリンクです。.env ファイルはLaravelの環境変数で構成されています。

production server app directory

current ディレクトリに移動すると、アプリケーションの内部が見えるはずです。ご覧のように、.env/var/www/app/.env ファイルを指しており、storage/var/www/app/storage/ ディレクトリを指しています。

production server current directory

結論

GitLab CI/CDを設定して自動テストを実行し、継続的デリバリーの手法を使ってEnvoyを使ったLaravelアプリケーションをコードベースから直接本番環境にデプロイしました。

Envoyはまた、私たちのカスタムBashスクリプトを書いたり、Linuxの魔法を使ったりすることなく、アプリケーションをデプロイするのに役立ちました。