Blog

Gitlab CI とEnvoyで実現するダウンタイムゼロのCI〔継続的インテグレーション〕

先日、レガシーなPHP環境でもできる!EnvoyとGitlab CI / CD を使用した自動デプロイという記事を書きました。

今回は、Gitlab CI とEnvoyで実現するダウンタイムゼロのCI〔継続的インテグレーション〕ということで、Gitlab CI / CD の設定方法とEnvoyでのタスクの記述方法などを紹介したいと思います。

ここで紹介する方法自体は、Gitlabの公式ドキュメントでも紹介されています。公式ドキュメントでは、Laravelを使用した前提になっていますが、 実際は、Laravelを使用していなくても実装できるので、その辺りも踏まえて紹介していきたいと思います。

実際は、事前準備などがありますが、ダウンタイムゼロの自動デプロイメントの流れを超ざっくり書くと以下のようになっています。アプリケーションのアップデートに必要なコマンド実行などの操作をすべて行って正常稼働できる状態になってからドキュメントルートを切り替えるという方法を用いるため、ダウンタイムなくリリースすることが可能になります。

  1. 最新リリース用の新しいディレクトリーを作成
  2. 最新リリース用のディレクトリーに git clone する
  3. ここで、アプリケーションのアップデートに必要なコマンドなどを実行
  4. ドキュメントルートを最新リリース用ディレクトリに変更

デプロイ用のユーザーを作成

まず、リリース対象のサーバーでデプロイ用のユーザーを作成します。 ユーザー名は、任意の名前で構いませんが、用途がはっきりしている方が管理上良いので、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_keyProject > Settings > RepositoryDeploy 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の記法で記述していきます。 基本的には、以下の要領でタスクを書いていきます。

  1. @serversで実行対象のサーバーを指定
  2. @setupでタスク内で使用できる変数を設定
  3. @taskで実行するタスクを記述
  4. @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セクションでは、デプロイ時に実行するタスクを指定していきます。

  1. まず、レポジトリーをclone
  2. 最新リリースをドキュメントルートになるようにシンボリックリンクを更新
  3. 最後に古いリリースを削除

この流れでタスクを書いていきます。以下は、単純なデプロイフローのサンプルですが、実際は、clone_repositoryupdate_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 > PermissionsContainer 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.ymlenvironment:で指定した環境のリストが表示されます。この画像は、その中のproductionに遷移したものですが、戻したいリリースのロールバックアイコンをクリックするだけで、指定のリリースにロールバックすることができます。

最後に、、、

CI/CDは、最初に設定することが多くて、難しそうに見えますが、一つずつ分解して実行していけば、そんなに難しくはないので、ぜひプロジェクトに取り入れてみてください。CI/CDを導入すると、アップロードミスやコマンドの実行ミスなどがなくなり、リリース作業が格段に楽になるかと思います。

For Every type of your business

開発やコンサルティングのご相談は、
お問い合わせフォームからお気軽に。