fablicでのLaravelとWordPressをデプロイする

### 概要
誰でも同じ操作で自動デプロイができるよう環境を構築する。

### 構成要件
rubyのcapistranoは以前にやったことがあるので、pythonのfabricを使ってみる。

* docker jenkins ( 公式コンテナ )
* python2.7 ( jenkinsコンテナ内でそのまま使えるため )
* python-pip
* python-setuptools

### デプロイ要件
* コマンドを使えない人でもデプロイできるように<font color="red">WebUIを用意する</font>（ Jenkins ）
* <font color="red">デプロイ</font>と<font color="red">ロールバック</font>の機能を提供する
* リリースコードは、履歴として<font color="red">５つ以上保持</font> (ロールバックできるように)
* <font color="red">docker環境</font>にもデプロイ可能に (ただし、プログラムコードをvolumeで外部共有)
* <font color="red">dockerを使っていない環境</font>にも対応 (Staging環境、本番環境)

### ファイル構成 (デプロイ関連)
ファイルの構成は、以下のようにしました。

```:ファイル構成
├ README.md
├ docker-compose.yml
├ jenkins
│  └ Dockerfile
└ deploy
   ├ config
   │  ├ development.py
   │  ├ production.py
   │  └ testing.py
   └ fabfile.py
```

### ファイル作成(docker-compose.yml)
```yaml:docker-compose.yml
version: '2'
services:
 jenkins:
  build: jenkins/.
  container_name: 'jenkins'
  working_dir: '/root/'
  ports:
   - '8080:8080'
  volumes:
   - jenkins-data:/var/jenkins_home
   - ./deploy:/root/deploy

volumes:
  jenkins-data:
    driver: local
```

### ファイル作成(Dockerfile)
```:jenkins/Dockerfile
FROM jenkins
USER root

# system update
RUN apt-get -y update && apt-get -y upgrade

# locale
RUN apt-get -y install locales && \
    localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8

# add tools to work.
RUN apt-get install -y vim less

# fabric
RUN apt-get install -y python-pip python-setuptools
RUN pip install setuptools --no-use-wheel --upgrade
RUN pip install fabric
```

### ファイル作成（fabfile.py）

```py:jenkins/opt/fabfile.py
# coding: utf-8

from __future__ import with_statement
from fabric.api import env, task, local, lcd, put, run, cd, abort
from fabric.contrib.files import exists
from fabric.contrib.console import confirm

import time
import config.production
import config.staging
import config.development


"""
  コマンド例)
  
  ex) 本番環境
    $fab production deploy:tag=20180101-01
    $fab production rollback

  ex) テスト環境
    $fab staging deploy:branch=master
    $fab staging rollback
    
  ex) 開発環境
    $fab development deploy:branch=feature/hoge
    $fab development rollback
"""


@task
def development():
    env.environment = 'development'
    config.development.init()


@task
def staging():
    env.environment = 'staging'
    config.staging.init()


@task
def production():
    env.environment = 'production'
    config.production.init()


@task
def deploy(tag='',branch=''):

    # tag/branchの指定 ※ tag優先
    if len(tag) > 0:
        env.git_type = 'tag'
        env.git_target = tag
    elif len(branch) > 0:
        env.git_type = 'branch'
        env.git_target = branch
    else:
        env.git_type = ''
        env.git_target = ''
    if len(env.git_target) < 1:
        abort("ブランチ/タグが指定されていません")

    # お知らせ表示
    print("""
    ＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／
    
      env    : %(environment)s
      host   : %(host)s
      repo   : %(repo)s
      target : %(target)s (%(type)s)
    
    ＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／
    """ % {
        'environment': env.environment,
        'host': str(env.hosts),
        'repo': env.git_repository,
        'type': env.git_type,
        'target': env.git_target
    })

    # デプロイ開始
    init()
    checkout()
    setup()
    symlink()
    cleanup()


@task
def rollback(is_confirm=True):
    # お知らせ表示
    print("""
    ＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／
    
      env : %(environment)s
      host: %(host)s
      repo: %(repo)s
    
    ＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／＿／
    """ % {'environment': env.environment, 'host': str(env.hosts), 'repo': env.git_repository})

    # 実行確認
    if is_confirm == True and not confirm("[" + env.environment + "] ロールバックします、よろしいですか？"):
        abort("中止しました")

    init()
    rollback_code()


"""
 初期化
 ※ パス生成 & 存在確認(自動フォルダ作成)
"""
def init():
    # 各フォルダ名の定義
    env.dir_deploy = str(int(time.time()) * 1000)
    env.dir_releases = 'releases'
    env.dir_current = 'current'
    env.dir_shared = 'shared'
    env.dir_logs = 'logs'

    # パス生成 (アプリケーションフォルダ)
    env.app_path = env.base_path + '/' + env.dir_app

    # パス生成と存在確認 (releasesフォルダ)
    releases_path = env.app_path + '/' + env.dir_releases
    if not exists(releases_path):
        run('mkdir -p ' + releases_path)

    # パス生成と存在確認 (sharedフォルダ)
    shared_path = env.app_path + '/' + env.dir_shared
    if not exists(shared_path):
        run('mkdir -p ' + shared_path)

    # パス生成と存在確認 (logsフォルダ)
    logs_path = env.app_path + '/' + env.dir_logs
    if not exists(logs_path):
        run('mkdir -p ' + logs_path)

    # パス生成 (デプロイ対象フォルダ)
    env.current_release = env.app_path + '/' + env.dir_releases + '/' + env.dir_deploy

    # パス生成 (作業フォルダ)
    env.tmp_path = '/tmp/%(app)s-%(dir)s' % {'app': env.dir_app, 'dir': env.dir_deploy}


"""
 コード配置
"""
def checkout():
    # フォルダ作成 (リリース先)
    run("mkdir -p %(release_dir)s" % {'release_dir': env.current_release})
    with cd(env.current_release):

        # 作業フォルダ作成 (ローカル)
        tmp = env.tmp_path + '/code'
        local('mkdir -p %(tmp)s' % {'tmp': tmp})
        with lcd(tmp):
            # 圧縮ファイルのパス生成
            gzip = env.dir_app + '.tar.gz'
            gzip_path = env.tmp_path + '/' + gzip

            # コード展開 && 圧縮
            if env.git_cmd == 'archive':
                # git archive ... ※ gitコマンド使えないが軽い
                local('git clone -q git@github.com:%(git_repository)s .' % {'git_repository': env.git_repository})
                if env.git_type == 'tag':
                    local("git archive %(target)s --format=tar.gz > %(gzip_path)s" % {'target': env.git_target, 'gzip_path': gzip_path})
                else:
                    local("git checkout -q %(target)s" % {'target': env.git_target})
                    local("git archive HEAD --format=tar.gz > %(gzip_path)s" % {'target': env.git_target, 'gzip_path': gzip_path})

            elif env.git_cmd == 'clone':
                # git clone ... ※ gitクライアント: 1.8以上
                local("git clone -q --depth 1 -b '%(target)s' %(url)s ." % {
                    'target': env.git_target,
                    'url': "https://%(git_user)s:%(git_access_token)s@github.com/%(git_repository)s" % {
                        'git_user': env.git_user,
                        'git_access_token': env.git_access_token,
                        'git_repository': env.git_repository
                    }
                })
                local('tar czf %(gzip_path)s .' % {'gzip_path': gzip_path})
            else:
                # git checkout ...
                local('git clone -q git@github.com:%(git_repository)s .' % {'git_repository': env.git_repository})
                if env.git_type == 'tag':
                    local('git checkout -q -b tag%(tag)s refs/tags/%(tag)s' % {'tag': env.git_target})
                else:
                    local('git checkout -q %(branch)s' % {'branch': env.git_target})
                local('tar czf %(gzip_path)s .' % {'gzip_path': gzip_path})

            # 転送 && 配置(解凍)
            put(gzip_path, env.current_release)
            run('tar xzf %(code)s && rm %(code)s' % {'code': gzip})

            # バージョン情報ファイル作成
            run('echo "repository: %(repository)s" > version.txt' % {'repository': env.git_repository})
            run('echo "type      : %(type)s" >> version.txt' % {'type':env.git_type})
            run('echo "target    : %(target)s" >> version.txt' % {'target':env.git_target})

            # 環境設定ファイル (WordPress)
            run("ln -nfs ../../../../%(shared)s/wordpress/wp-config.php ./httpd/article/wp-config.php" % {'shared': env.dir_shared})

            # 環境設定ファイル (Laravel)
            run("ln -nfs ../../../%(shared)s/laravel/.env ./laravel/.env" % {'shared': env.dir_shared})

            # phpunit.xml (Laravel ※注意:コピー)
            run("rm -rf %(logs)s && cp %(shared)s %(logs)s" % {
                'shared': './../../' + env.dir_shared + '/laravel/phpunit.xml',
                'logs': './laravel/phpunit.xml'
            })

            # 作業フォルダ削除 (ローカル)
            local('cd /tmp/ && rm -rf %(tmp)s' % {'tmp':env.tmp_path})


"""
 PHPセッティング
 ※ PHPのライブラリをインストール(composer)
 ※ migtation (laravel)
"""
def setup():
    if env.environment in ['staging', 'production']:
        # 本番・テスト環境 (オンプレ)
        with cd(env.current_release + '/laravel'):
            # composer
            run('php composer.phar config -g github-oauth.github.com "%(token)s"' % {'token':env.git_access_token})
            run('php composer.phar install -q')
            run('php artisan migrate --force')
    else:
        # 開発環境 (docker)
        dir = "%(base)s/%(app)s/%(releases)s/%(dir)s/laravel" % {
            'base': env.docker_path,
            'app': env.dir_app,
            'releases': env.dir_releases,
            'dir': env.dir_deploy
        }
        run('docker exec -u www-data php bash -c "cd %(dir)s && php composer.phar config -g github-oauth.github.com %(token)s"' % {'dir': dir, 'token': env.git_access_token})
        run('docker exec -u www-data php bash -c "cd %(dir)s && php composer.phar install -q"' % {'dir': dir})
        run('docker exec -u www-data php bash -c "cd %(dir)s && php artisan migrate"' % {'dir': dir})


"""
 シンボリックリンク
 ※ 相対パスで指定する (docker環境でエラーとなる)
"""
def symlink():
    with cd(env.app_path):
        app = './' + env.dir_releases + '/' + env.dir_deploy

        # [Laravel] アプリケーションログ
        run("rm -rf %(logs)s && ln -nfs %(shared)s %(logs)s" % {
           'shared': '../../../../' + env.dir_logs,
           'logs': app + '/laravel/storage/logs'
        })

        # [WordPress] wp-content/uploads
        run("ln -nfs %(shared)s %(app)s" % {
           'shared': '../../../../../' + env.dir_shared + '/wordpress/uploads',
           'app': app + '/httpd/article/wp-content/uploads',
        })

        # 本番・テスト環境のみ
        if env.environment in ['staging', 'production']:
            # [WordPress] .htaccess
            run("rm -rf %(app)s && ln -nfs %(shared)s %(app)s" % {
                'shared': '../../../' + env.dir_shared + '/wordpress/.htaccess',
                'app': app + '/httpd/.htaccess',
            })

            # [WordPress] wp-content/plugins
            run("rm -rf %(app)s && ln -nfs %(shared)s %(app)s" % {
                'shared': '../../../../../' + env.dir_shared + '/wordpress/plugins',
                'app': app + '/httpd/article/wp-content/plugins',
            })

        # 切り替え (新しいコード)
        run("ln -nfs %(app)s %(current)s" % {'app': app, 'current': env.dir_current})


"""
 後片付け 
 ※ 履歴フォルダ削除
"""
def cleanup():
    with cd(env.app_path):
        keep = env.keep_releases
        releases_path = './' + env.dir_releases
        releases = sorted(run('ls -x %(releases)s' % {'releases':releases_path}).split())

        # 5つ以上の場合は、旧バージョンを削除する
        if (len(releases)) > keep:
            releases.reverse()
            del releases[:keep]
            directories = ' '.join(["%(releases)s/%(dir)s" % {'releases': releases_path, 'dir': dir} for dir in releases])
            run("rm -rf %(directories)s" % {'directories': directories })


"""
 ロールバック
 ※ 旧コードへ戻す (DBは戻りません)
"""
def rollback_code():
    with cd(env.app_path):
        releases_path = './' + env.dir_releases
        releases = sorted(run('ls -x %(releases)s' % {'releases': releases_path}).split())
        if len(releases) >= 2:
            current = "%(releases)s/%(current)s" % {'releases': releases_path, 'current': releases[-1]}
            previous = "%(releases)s/%(previous)s" % {'releases': releases_path, 'previous': releases[-2]}
            run("ln -nfs %(previous)s %(current)s" % {'previous': previous, 'current': './' + env.dir_current})
            run("rm -rf %(directory)s" % {'directory': current})
        else:
            abort("旧バージョンがないため、中止しました。")

```

### ファイル作成（各環境の設定ファイル）
#### 開発環境 (Vagrant + Docker環境)
```py:config/development.py
from fabric.api import env


def init():
    # server
    env.hosts = ['192.168.0.1']
    env.port = 22
    env.user = 'hoge'
    env.key_filename = '~/.ssh/id_rsa'
    env.password = ''

    # directory
    env.dir_app = 'www.example.com'
    env.base_path = '/home/vagrant/opt'
    env.docker_path = '/var/www'
    env.keep_releases = 5

    # git repository
    env.git_cmd = 'clone'
    env.git_user = 'hoge'
    env.git_access_token = '***********************************'
    env.git_repository = 'hoge/www.example.com.git'
```
#### ステージング環境 (オンプレ - Linux環境)
```py:config/staging.py
from fabric.api import env


def init():
    # server
    env.hosts = ['172.16.0.1']
    env.port = 22
    env.user = 'hoge'
    env.key_filename = '~/.ssh/id_rsa'
    env.password = ''

    # directory
    env.dir_app = '/www.example.com'
    env.base_path = '/var/www'
    env.docker_path = ''
    env.keep_releases = 5

    # git repository
    env.git_cmd = 'clone'
    env.git_user = 'hoge'
    env.git_access_token = '***********************************'
    env.git_repository = 'hoge/www.example.com.git'
```

#### 本番環境 (オンプレ - Linux環境)
```py:config/production.py
from fabric.api import env


def init():
    # server
    env.hosts = ['10.0.10.1']
    env.port = 22
    env.user = 'hoge'
    env.key_filename = '~/.ssh/id_rsa'
    env.password = ''

    # directory
    env.dir_app = '/www.example.com'
    env.base_path = '/var/www'
    env.docker_path = ''
    env.keep_releases = 5

    # git repository
    env.git_cmd = 'clone'
    env.git_user = 'hoge'
    env.git_access_token = '***********************************'
    env.git_repository = 'hoge/www.example.com.git'

```

### ファイル構成 (デプロイされるアプリ)
fabricでデプロイされるアプリ側のファイル構成は下記のようになります。

```:ファイル構成
├ current      <- シンボリックリンク (releasesフォルダの最新フォルダへ)
│
├ releases     <- リリースごとにフォルダ分け
│  ├ 20171231120102
│  ├ 20171231120512
│  ├ 20171231120845
│  ├ 20171231120845
│  └ 20180101000005
│
└ shared
   ├ logs
   ├ laravel
   │  └ .env
   └ wordpress
      ├ .htaccess
      ├ wp-config.php
      ├ plugins
      └ uploads
```

### 環境構築
#### Dockerコンテナの起動
```
$ docker-compose up -d
$ docker exec -it jenkins bash
```

### コマンドについて
jenkinsコンテナへ接続して下記コマンドを実行することでデプロイ・ロールバックができる。

#### 開発環境
```
$fab development deploy:branch=feature/hoge
$fab development rollback
```
#### ステージング環境
```
$fab staging deploy:branch=master
$fab staging rollback
```
#### 本番環境
```
$ fab production deploy:tag=20180101-01
$ fab production rollback
```

以上

### 参考サイト
* [Python製デプロイツール Fabricを初めて使う際に役立つ](http://dekokun.github.io/posts/2013-04-07.html)
* [fabric - fab オプションと引数](http://fabric-ja.readthedocs.io/ja/latest/usage/fab.html)
* [fabric - タスクの定義](http://fabric-ja.readthedocs.io/ja/latest/usage/tasks.html)
* [Qiita - Fabricインストール](https://qiita.com/Kuchitama/items/18b63271bf706e34e8b1)
* [CentOSにfabricの最新版をインストール](http://www.karakaram.com/centos6-fabric-install)
* [Fabricで接続情報を指定する方法いろいろ＆FabricからPHPスクリプトを実行してみたメモ](http://k-holy.hatenablog.com/entry/2013/04/09/202545)

* http://blog.digital-squad.net/article/405317288.html
* https://www.slideshare.net/kamekoopa/fabric-20004053
* https://gist.github.com/mahmoudimus/384895
* https://serversforhackers.com/c/deploying-with-fabric

* [Dockerのコンテナ間でssh接続させる](https://qiita.com/3104k/items/16102a64fb1bae6665af)
* [https+アクセストークンを使ってGitHubのアカウントを使い分ける](https://qiita.com/tq_jappy/items/6e2f81f372e4abaa5139)

* [Fabric を pyenv + virtualenv 環境で使ったら楽しかったよ！](http://snickerjp.blogspot.jp/2014/12/python-fabric-pyenv-virtualenv.html)
* [Python で仮想環境管理 venv の使い方](https://qiita.com/fiftystorm31/items/b2fd47cf32c7694adc2e)
* [Python2とPython3でvirtualenv環境を作成する手順メモ](https://hnakamur.github.io/blog/2015/07/26/how_to_use_venv_with_homebrew_python2_and_3/)
* [git archive を用いて clone せずに特定のディレクトリだけを取得する](https://qiita.com/Yuki-Inoue/items/b2eae79eae37e8c41b31)
* [gitでタグを指定してチェックアウト](https://qiita.com/Arvelt/items/b4fd814bb1c68bfaacc8)