先日、レガシーなPHP環境でもできる!EnvoyとGitlab CI / CD を使用した自動デプロイという記事を書きました。
今回は、Gitlab CI とEnvoyで実現するダウンタイムゼロのCI〔継続的インテグレーション〕ということで、Gitlab CI / CD の設定方法とEnvoyでのタスクの記述方法などを紹介したいと思います。
ここで紹介する方法自体は、Gitlabの公式ドキュメントでも紹介されています。公式ドキュメントでは、Laravelを使用した前提になっていますが、 実際は、Laravelを使用していなくても実装できるので、その辺りも踏まえて紹介していきたいと思います。
実際は、事前準備などがありますが、ダウンタイムゼロの自動デプロイメントの流れを超ざっくり書くと以下のようになっています。アプリケーションのアップデートに必要なコマンド実行などの操作をすべて行って正常稼働できる状態になってからドキュメントルートを切り替えるという方法を用いるため、ダウンタイムなくリリースすることが可能になります。
- 最新リリース用の新しいディレクトリーを作成
- 最新リリース用のディレクトリーに
git clone
する - ここで、アプリケーションのアップデートに必要なコマンドなどを実行
- ドキュメントルートを最新リリース用ディレクトリに変更
デプロイ用のユーザーを作成
まず、リリース対象のサーバーでデプロイ用のユーザーを作成します。
ユーザー名は、任意の名前で構いませんが、用途がはっきりしている方が管理上良いので、deployer
という名前でユーザーを作成しています。また、リリース作業を行うディレクトリは、/var/www
としています。
# deployer のユーザー名で作成
sudo adduser deployer
# deployerユーザーに リリース作業を行うディレクトリに rwx の権限を付与
sudo setfacl -R -m u:deployer:rwx /var/www
SSH キーの登録
SSHキーの作成方法は、割愛させていただきますが、deployer ユーザー用のSSHキーをパスフレーズなしで作成しておきます。
# リリース対象のサーバーでdeployer ユーザーとして実行します。
#
# public key を authorized_keys へコピーします。
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
# 表示された private key のテキストをコピーしておきます。
cat ~/.ssh/id_rsa
先ほどコピーしたprivate key
をGitlabプロジェクトのSettings > CI/CDの変数に登録しておきます。Keyには、SSH_PRIVATE_KEY
を入力し、Valueに先ほどコピーしたprivate key
をペーストします。
次にpublic_key
もProject > Settings > RepositoryのDeploy Key
として登録しておく必要があります。
# リリース対象のサーバーでdeployer ユーザーとして実行します。
#
# 表示された public key をコピーします。
cat ~/.ssh/id_rsa.pub
各種キーの登録が完了したら、リリース対象のサーバーからgitへアクセスできるか確認しておきます。
# リリース対象のサーバーでdeployer ユーザーとして実行します。
#
git clone git@gitlab.example.com:<USERNAME>/some-project.git
もし Are you sure you want to continue connecting (yes/no)?
を聞かれたら yes
と入力しておきます。
もし、正常にcloneできない場合は、設定ミスなどが考えられるので、先ほど登録した各種キーなどが正しく設定されているか、確認してください。
リリース対象のサーバー設定
サーバーは、NginxでもApacheでも問題ありませんが、ドキュメントルートを変更する必要があります。 現在の設定が、/var/www/public_html/public
となっている場合は、/var/www/public_html/current/public
となるように変更します。この構成は、Laravelのようにプロジェクト内にドキュメントルートのディレクトリが存在する場合の例ですが、プロジェクトルート=ドキュメントルートとなっているような場合は、現在のドキュメントルートcurrent
を付け加えて /var/www/public_html/current
となるように変更します。
Apacheの設定ファイルで言うとこんな感じになります。
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/public_html/current
・
・
・
その他の設定
</VirtualHost>
プロジェクトディレクトリの構成変更
ローカル開発時のプロジェクトディレクトリの構成は、変更する必要ありませんが、CI / CD の導入に伴い、リリース対象のサーバーでのプロジェクトディレクトリの構成を変更する必要があります。
現在の構造
現在の構造が、以下のようになって、レポジトリーで管理していないファイルおよびディレクトリが、.env
、.htaccess
と/storage/
だったとします。現状は、リリース対象のサーバー内も同一の構造をしていると思いますが、
サーバーのドキュメントルートを/var/www/public_html/current
に変更したため、プロジェクトディレクトリの構造を変更する必要があります。
.
├── .env
├── .htaccess
├── Dockerfile
├── Envoy.blade.php
├── index.php
├── plugins
├── storage
├── tests
├── themes
└── vendor
変更後の構造
すべてのリリースは、releases
の中に格納されるようになるので、プロジェクトディレクトリの構造は、以下のようになります。デプロイ時に最新のリリースが、current
にシンボリックリンクされます。.env
、.haccess
と/storage/
は、releases
と同階層に配置し、デプロイ時のタスクで最新リリース内にシンボリックリンクを貼るようにします。
.
├── .env
├── .htaccess
├── storage
├── releases
└── current (シンボリックリンク、デプロイ時に作成される)
Envoyのタスクを設定
サーバーの設定などが完了したので、次はデプロイを行うEnvoyのタスクを設定していきます。
Envoyタスクの記述方法
タスクの記述は、Laravelでお馴染みのbladeの記法で記述していきます。 基本的には、以下の要領でタスクを書いていきます。
- @serversで実行対象のサーバーを指定
- @setupでタスク内で使用できる変数を設定
- @taskで実行するタスクを記述
- @storyでデプロイ時に実行するタスクの順序を指定
タスクを書いたファイルは、プロジェクトルートに Envoy.blade.php
として保存しておきます。
@servers([‘web’ => ‘deployer@remotehost’])
@setup
$repository = ‘git@example.com:<USERNAME>/some-project.git’;
$releases_dir = ‘/var/www/public_html/releases’;
$app_dir = ‘/var/www/public_html’;
$release = date(‘YmdHis’);
$new_release_dir = $releases_dir .’/‘. $release;
@endsetup
@story(‘deploy’)
clone_repository
clean_old_releases
@endstory
@task('clone_repository')
echo 'Cloning repository'
[ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
git clone {{ $repository }} {{ $new_release_dir }}
cd {{ $new_release_dir }}
git reset --hard {{ $commit }}
@endtask
@task(‘clean_old_releases’)
# Delete all but the 10 most recent releases
echo ‘Cleaning old releases’
cd {{ $releases_dir }}
ls -dt {{ $releases_dir }}/* | tail -n +11 | xargs -d “\n” rm -rf;
@endtask
上記の内容で、以下のコマンドを実行すると、clone_repository
のタスクが実行され、次にclean_old_releases
が実行されます。
envoy run deploy
@storyでまとめてタスクを実行しましたが、もちろん個別のタスクを実行することも可能です。また、ローカル環境で実行する場合には、Envoyをインストールしておく必要があります。
composer global require laravel/envoy
開発サーバーと本番サーバーでタスクを分ける場合
@serversで開発サーバーと本番サーバーを定義して、@taskでどちらのサーバーで実行するかを指定することができます。
@servers([‘dev’ => ‘remote_username@remote_host’,‘live’ => ‘remote_username@remote_host’])
@task(‘list_dev’, [‘on’ => ‘dev’])
ls -l
@endtask
@task(‘list_live’, [‘on’ => ‘live’])
ls -l
@endtask
ダウンタイムゼロのためのデプロイタスク
ダウンタイムゼロを実現するためのタスクをEnvoyに書いていきます。
@setup
@setup セクションでは、デプロイ時に使用する変数を設定します。
@setup
$repository = ‘git@example.com:<USERNAME>/some-project.git’;
$releases_dir = ‘/var/www/public_html/releases’;
$app_dir = ‘/var/www/public_html’;
$release = date(‘YmdHis’);
$new_release_dir = $releases_dir .’/‘. $release;
@endsetup
- $repository これは、プロジェクトのレポジトリーを指定
- $releases_dir これは、どのディレクトリにデプロイするかを指定
- $app_dir アプリケーションディレクトリを指定
- $release 日付ベースのディレクトリが作成され、そのに各リリースが格納されます。
- $new_release_dir デプロイ時における最新のリリースディレクトリを指定
@story
@storyセクションでは、デプロイ時に実行するタスクを指定していきます。
- まず、レポジトリーをclone
- 最新リリースをドキュメントルートになるようにシンボリックリンクを更新
- 最後に古いリリースを削除
この流れでタスクを書いていきます。以下は、単純なデプロイフローのサンプルですが、実際は、clone_repository
とupdate_symlinks
の間に必要なコマンド群を列挙します。
@story(‘deploy’)
clone_repository
update_symlinks
clean_old_releases
@endstory
レポジトリーをclone
$releases_dir
をなければ作成して、$new_release_dir
にレポジトリーをクローンします。
@task('clone_repository')
echo 'Cloning repository'
[ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
git clone {{ $repository }} {{ $new_release_dir }}
cd {{ $new_release_dir }}
git reset --hard {{ $commit }}
@endtask
最新リリースを有効化する
update_symlinks
タスク内では、レポジトリー管理していないディレクトリやファイルを使用できるようにシンボリックリンクを作成もしくは、更新します。
.env
ファイル、.htaccess
ファイル、画像などのメディアファイルがアップロードされるディレクトリなどが、これに該当するかと思います。
@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 .htaccess’
ln -nfs {{ $app_dir }}/.htaccess {{ $new_release_dir }}/.htaccess
@endtask
古いリリースを削除する
サーバーリソースが無限にある場合は良いですが、そうではない場合がほとんどかと思います。
@task(‘clean_old_releases’)
# 最新10リリース以外を削除
echo ‘Cleaning old releases’
cd {{ $releases_dir }}
ls -dt {{ $releases_dir }}/* | tail -n +11 | xargs -d “\n” rm -rf;
@endtask
この例では、10リリースを保持していますが、万が一のロールバックなども考えて、保存しておくリリースの数を決めてください。
最終的なEnvoy
シンプルなデプロイタスクですが、最終的な Envoy.blade.php
はこんな感じになります。
@servers([‘web’ => ‘deployer@remotehost’])
@setup
$repository = ‘git@example.com:<USERNAME>/some-project.git’;
$releases_dir = ‘/var/www/public_html/releases’;
$app_dir = ‘/var/www/public_html’;
$release = date(‘YmdHis’);
$new_release_dir = $releases_dir .’/‘. $release;
@endsetup
@story(‘deploy’)
clone_repository
update_symlinks
clean_old_releases
@endstory
@task('clone_repository')
echo 'Cloning repository'
[ -d {{ $releases_dir }} ] || mkdir {{ $releases_dir }}
git clone {{ $repository }} {{ $new_release_dir }}
cd {{ $new_release_dir }}
git reset --hard {{ $commit }}
@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 .htaccess’
ln -nfs {{ $app_dir }}/.htaccess {{ $new_release_dir }}/.htaccess
@endtask
@task(‘clean_old_releases’)
# Delete all but the 10 most recent releases
echo ‘Cleaning old releases’
cd {{ $releases_dir }}
ls -dt {{ $releases_dir }}/* | tail -n +11 | xargs -d “\n” rm -rf;
@endtask
Gitlab CI / CDの設定
ここまでくると、ローカルでEnvoyタスクを実行すれば、デプロイすることが可能になりましたが、gitのpushなどのイベントにフックさせて自動デプロイメントを実現するために CI / CD を設定していきます。
Dockerコンテナイメージの作成
Envoyが動作する環境が構築できるDockerfile
をプロジェクトルートに設定します。
# Set the base image for subsequent instructions
FROM php:7.1
# 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”
GitLab Container Registry の設定
Dockerfileができたので、それをビルドしてGitLab Container Registryにプッシュします。
Packages > Container registryの画面を開きます。 もし、このメニューが存在しない場合は、Settings > General > Permissionsで Container registryを有効化しておきます。
Gitlab上のContainer registryを使用するには、Gitlabのユーザー名とパスワードを使用してGitLab registryにログインします。
docker login registry.gitlab.com
DockerfileをビルドしてGitLab registryにプッシュします。 それぞれのコマンドは、そこそこ時間がかかるので、コーヒーでも飲みながら気長に待ちましょう。
docker build -t registry.gitlab.com/<USERNAME>/some-project .
docker push registry.gitlab.com/<USERNAME>/some-project
問題なくプッシュできると、Packages > Container registryの画面にこんな感じに表示されます。
作成したDockerfileは、プロジェクトのレポジトリーにプッシュしておきます。
git add Dockerfile
git commit -m ‘Add Dockerfile’
git push origin master
GitLab CI/CDの設定ファイルを作成する
GitLab CI/CDを使用するには、その挙動を制御する.gitlab-ci.yml
という名前の設定ファイルをプロジェクトルートに設置しておく必要があります。
まず、ベースとして使用するimageを指定します。今回のケースで言うと先ほど作成したものを使用します。また、それとは別に追加で使用するimageをserviceとして追加することができます。
実際にパターンとして、developブランチにプッシュしたら自動でテストを実行して、通れば自動で開発サーバーにデプロイ。masterブランチにマージされた場合のデプロイは手動というのをよく使用します。
サンプルとして掲載しておくので参考にしてください。
url
などを適宜環境に合わせて変更してもらえれば、実際に使用することもできるかと思います。
もし、デプロイのみを行ってテストはしないような場合は、単純に- test
を削除、unit_test
のブロックを削除、そしてテストしないので不要になるservices
のブロックとvariables
のブロックを削除すれば、デプロイのみ行うことができます。
image: registry.gitlab.com/<USERNAME>/some-project: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_staging:
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_dev —commit=“$CI_COMMIT_SHA”
environment:
name: staging
url: https://dev.example.com
only:
- develop
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: https://example.com
when: manual
only:
- master
Gitlab CI / CD のジョブ
先ほどの.gitlab-ci.yml
を使用した場合、developブランチにプッシュされると、自動的にCI / CD のジョブが実行されます。CI / CD > Jobs にジョブの一覧が表示されます。
今度は、masterブランチにマージすると、ジョブは自動実行されずに保留されているのが分かります。デプロイは、再生ボタンをクリックするだけで行えます。
もしもの時のロールバック
Operations > Environments に .gitlab-ci.yml
のenvironment:
で指定した環境のリストが表示されます。この画像は、その中のproductionに遷移したものですが、戻したいリリースのロールバックアイコンをクリックするだけで、指定のリリースにロールバックすることができます。
最後に、、、
CI/CDは、最初に設定することが多くて、難しそうに見えますが、一つずつ分解して実行していけば、そんなに難しくはないので、ぜひプロジェクトに取り入れてみてください。CI/CDを導入すると、アップロードミスやコマンドの実行ミスなどがなくなり、リリース作業が格段に楽になるかと思います。