監視とかで定期的に SSH するなら ControlMaster を設定するとスッキリするけど常用はちょっと

Cacti の Percona のテンプレートを使う場合、Cacti から監視対象のサーバに SSH で接続してメトリクスを取ってくることになるのですが・・・普通にやると都度サーバにSSH接続する事になりますし、対象サーバの /var/log/secure にログが無駄に記録されて辛いです。

そこで ControlMaster を使います。

ControlMaster とは

適当なディレクトリに ssh_config を下記のように作成します。

ControlMaster auto
ControlPath ~/.ssh/mux-%r@%h:%p
ControlPersist 10m

ssh -F ssh_config ... のようにこのファイルを指定して対象サーバに接続します。すると・・・

  • 最初に接続しようとしたときにバックグラウンドでマスター接続を張るプロセスが起動する
  • このマスター接続は ~/.ssh/mux-%r@%h:%p というファイル名でソケットファイルを作る
  • このソケットファイル上のマスター接続を用いて実際に SSH セッションは執り行われる
  • 次回以降の接続もソケットファイルが存在するなら自動的に使用される
  • 10m 以上マスター接続が使われなかったら切断されてソケットファイルも消える

つまり、1つの TCP のセッション上で複数の SSH のセッションが張られるようになる、みたいな感じです。

# バックグラウンドでマスター接続を張るプロセスが起動してその接続上で SSH セッションが張られる
ssh 192.0.2.123 uname -n

# ↑で作られたマスター接続のソケットファイルの接続上で SSH セッションが張られる
ssh 192.0.2.123 uname -n

# 同上
ssh 192.0.2.123 uname -n

:
:

# しばらく(↑の設定だと 10 分)放置するとマスター接続が切断される

# 新たにマスター接続を張るプロセスが起動する
ssh 192.0.2.123 uname -n

詳しくは・・・ControlMaster とか参照。

ControlMaster が有効な場合は /var/log/secure には最初のマスター接続のときだけログが記録されるので、ログもスッキリします。

常用はしないほうが良い

とても便利な ControlMaster ですが、普段から有効にしておけば同じサーバに何度も接続するときに接続のオーバーヘッドが小さくなるので、ユーザーの ~/.ssh/config で下記のようにしたくなりますが・・・

ControlMaster auto
ControlPath ~/.ssh/mux-%r@%h:%p
ControlPersist 10m

正直オススメしません。。。次のようなケースで大事故の元になります。

  • 本番系と検証系で同じ IP アドレスになっている
  • 直接接続することはできず中継サーバを挟む必要がある
  • どちらの系に接続するかは経由する中継サーバによって変わる
  • 例えば中継サーバは下記のアドレスだとすると・・・
    • 本番系に接続するときは 192.0.2.100 を経由
    • 検証系に接続するときは 192.0.2.200 を経由

ProxyCommand を使って下記のように本番系に接続したとします。

ssh -o "ProxyCommand ssh hj@192.0.2.100 -W %h:%p 2>/dev/null" ore@10.21.10.4

このとき、下記のようなソケットファイルが作成されます。

  • ~/.ssh/mux-ore@192.0.2.100:22
  • ~/.ssh/mux-ore@10.21.10.4:22

次に、検証系に次のように接続しようとします。サーバのアドレスは同じ(10.21.10.4)ですが、中継サーバのアドレスが異なるので検証系に接続されるはずです。

ssh -o "ProxyCommand ssh hj@192.0.2.200 -W %h:%p 2>/dev/null" ore@10.21.10.4

がしかし、実際には ControlMaster の機能により ~/.ssh/mux-ore@10.21.10.4:22 というソケットファイルが発見され、このソケットファイルを使用して SSH セッションが張られます。その接続先は本番系です。

つまり、検証系に接続するつもりが、本番系に接続されます。

さいごに

Nagios の check_ssh プラグインは ssh コマンドではなくプラグインが直接 TCP で接続しにいくので ControlMaster とか全然関係ないですしそもそも ControlMaster が有効では監視の意味がないです。

ので、Cacti からのアクセスで ControlMaster を有効にして /var/log/secure への出力を抑止したとしても、Nagios のせいでわりと /var/log/secure が汚れました。

ssh なんてサービスに必要なものではないし、Nagios の SSH 監視は頻度を下げるとかで良いかと。


社内用に書いていたメモからのコピペ、1年ぐらい前に書いたメモでした。そもそも Cacti はもうほとんど使ってないです(概ね Prometheus にした)。

「本番と検証でIPアドレスが同じで中継サーバが違う」という状況はよくあるように思うので(あんまり無い?)、常用はしないようにしてます。検証系のつもりで本番系に接続したことはないですけど、本番系のつもりで検証系に接続しててヒヤリしたことがあるので。

Jenkins でビルドのパラメータ化でブランチをフィルタしつつ自動でもビルドするメモ

文章で説明しにくい・・・要するに次のようにしたいとき。

  • masterissue/* のみをビルドの対象にする
  • Gitlab とかの WebHook で Jenkins の /git/notifyCommit を呼んでプッシュから自動ビルドさせる
  • 手動でビルドするときも↑のパターンにマッチするブランチを選択してビルドできる

自動と手動を併用しない、または両方ですべてのブランチを対象にする例はググるとよく見かける気がしたのですが、↑みたいなパターンでフィルタしつつ自動と手動を共存している設定例をあんまり見ない気がしたので、試行錯誤しました。

環境は下記の通り。

  • Jenkins 2.68
  • Jenkins plugins
    • Job DSL 1.63
    • Git Parameter Plug-In 0.8.0
    • Git plugin 3.3.1
    • Parameterized Trigger plugin 2.34
    • SCM API Plugin 2.1.1
    • etc…

次のように設定します。

  • ビルドのパラメータ化で Git Parameter を追加して・・・
    • Parameter TypeBranch を選択
    • Branch Filter に対象とするブランチを正規表現で指定
    • Default Value に↑の正規表現の頭に : を付けたものを指定
  • ソースコード管理の Git の ビルドするブランチ で↑のパラメータ名を指定する
    • パラメータ名が PARAM_GIT_BRANCH なら ${PARAM_GIT_BRANCH} のように指定

DSL だと次のような感じ。

job('test') {
    parameters {
        gitParameterDefinition {
            name('PARAM_GIT_BRANCH')
            type('BRANCH')
            // ↓の正規表現に `:` を付けたもの
            defaultValue(':^origin/(master|issue/[^/]+)$')
            description(null)
            branch(null)
            // ビルドするブランチの正規表現
            branchFilter('^origin/(master|issue/[^/]+)$')
            tagFilter(null)
            sortMode('ASCENDING_SMART')
            selectedValue('NONE')
            useRepository(null)
            quickFilterEnabled(false)
        }
    }
    scm {
        git {
            remote {
                url('git@git.example.com:oreore/testing.git')
                credentials('jenkins')
            }
            // ブランチに↑のパラメータを指定
            branches ('${PARAM_GIT_BRANCH}')
        }
    }
    triggers {
        scm('@midnight')
    }
}

手動でビルドするときは、存在するブランチから branchFilter の正規表現でフィルタされた候補が表示されて、そこから選択してビルドを開始できます。選択したブランチはソースコード管理の ビルドするブランチ にそのまま当てはめられます。

Gitlab の WebHook などから Jenkins の /git/notifyCommit を呼んで自動でビルドされるときは、パラメータが指定されないので、ビルドのパラメータの defaultValue で指定した値がソースコード管理の ビルドするブランチ に入ります。

ソースコード管理の ビルドするブランチ は先頭に : がついていると正規表現として解釈されるので branchFilter に指定した正規表現の頭に : を付けて defaultValue に指定することで、手動でビルドするときと自動でビルドされるときで同じブランチを対象にできます。


要するに、Branch FilterbranchFilter)が手動でビルドするときに選択候補に表示されるブランチ、Default Value(defaultValue)が自動でビルドするときに対象となるブランチ、になります。

ビルドのパラメータ化をしない場合は masterissue/* をビルドしたければ ビルドするブランチ に2行ブランチを指定するだけで良いのですが、

job('test') {
    scm {
        git {
            remote {
                url('git@git.example.com:oreore/testing.git')
                credentials('jenkins')
            }
            branches (
                'origin/master',
                'origin/issue/*',
            )
        }
    }
    triggers {
        scm('@midnight')
    }
}

ビルドをパラメータ化する場合は ビルドするブランチ にはパラメータ化した値が1つしか無いので↑みたいな2行の設定はできず、Default Value(defaultValue) に正規表現で指定する必要があります。


ちなみにブランチをフィルタせずにすべてのブランチを対象にする場合は次のようにしておくと良さそうです。

job('test') {
    parameters {
        gitParameterDefinition {
            name('PARAM_GIT_BRANCH')
            type('BRANCH')
            defaultValue('**')
            description(null)
            branch(null)
            branchFilter(null)
            tagFilter(null)
            sortMode('ASCENDING_SMART')
            selectedValue('NONE')
            useRepository(null)
            quickFilterEnabled(false)
        }
    }
    scm {
        git {
            remote {
                url('git@git.example.com:oreore/testing.git')
                credentials('jenkins')
            }
            // ブランチに↑のパラメータを指定
            branches ('${PARAM_GIT_BRANCH}')
        }
    }
    triggers {
        scm('@midnight')
    }
}

defaultValue を null とかにしてしまうと自動でビルドされるときに Couldn't find any revision to build. Verify the repository and branch configuration for this job. みたいになると思います。

Gitlab CI でカバレッジレポートを公開するメモ

Jenkins で PHPUnit 実行時に出力したカバレッジレポートを Jenkins 上で閲覧できるようにしているのですが、同じことを Gitlab CI でやるときのメモ。

Jenkins の場合

Jenkins なら HTML Publisher plugin でサクッと PHPUnit のカバレッジレポートを公開できます。

ビルドで↓のようにカバレッジレポートを HTML で出力して、

vendor/bin/phpunit --coverage-html=coverage/
# or
phpdbg -qrr vendor/phpunit/phpunit/phpunit --coverage-html=coverage/

ビルド後のアクションで↓みたいに公開するディレクトリを指定すれば OK です。

// jenkins dsl

job('test') {
    :
    publishers {
        publishHtml {
            report('coverage') {
                reportName('Coverage')
                reportFiles('index.html')
            }
        }
    }
}

Gitlab CI の場合

Gitlab CI で同じことをやるときは PHPUnit で出力したカバレッジレポートを Gitlab Pages で公開すると良いらしいです、下記によると。

下記のようなビルド設定でカバレッジレポートを Gitlab Pages で公開できます。

# .gitlab-ci.yml

before_script:
  - curl -fsSL https://getcomposer.org/download/1.4.2/composer.phar > composer.phar

phpunit:
  stage: test
  image: php:alpine
  script:
    - php composer.phar install --prefer-dist --no-progress --ansi --dev
    - phpdbg -qrr vendor/phpunit/phpunit/phpunit --coverage-html coverage/
  tags:
    - docker
  cache:
    paths:
      - vendor/
  # coverage/ ディレクトリを artifact として保存する
  artifacts:
    paths:
      - coverage/
    expire_in: 1 days

# Gitlab Pages のためのジョブ(pages という名前でなければならない)
pages:
  stage: deploy
  # phpunit ジョブの artifact を展開する
  dependencies:
    - phpunit
  # Pages で公開するファイルは public/ になければならない
  script:
    - mv coverage/ public/
  # artifact を移動させるだけならこうしとけば git clone/fetch や checkout されない
  variables:
    GIT_STRATEGY: none
  # Pages で公開するには artifact にしなければならない
  artifacts:
    paths:
      - public/
    expire_in: 1 days

Gitlab Pages のメモ

Gitlab Pages は要するに、pages というジョブのビルドの結果、 public というディレクトリが artifact として保存されれば、それが Gitlab Pages として公開される、です。GitHub Pages とはだいぶ感じが違います。

Gitlab Pages は最後にビルドされた結果しか公開されないので、ビルドごとのカバレッジレポートを閲覧したりはできなさそう。なのですべてのブランチでカバレッジレポートを保存するのではなく(Jenkins ではそうしてた)、master のときだけにしとくのが良さそう。トピックブランチのビルドで Gitlab Pages のカバレッジレポートが上書きされるのもどうかと思うので。

ただ、カバレッジレポートを artifact として保存しておけばダウンロードしてローカルで閲覧することはできるので、phpunit ジョブの artifact はそのままで、pages ジョブだけ特定ブランチに制限するのがよさそう。

ビルド履歴や成果物のメモ

artifact は有効期限を expire_in で指定しなければ無期限となるっぽい。expire_in を指定すると有効期限が切れると自動で削除されるが、ジョブの画面から Keep ボタンを押して有効期限をクリアする(無期限にする)こともできるっぽい。

成果物やビルドログはジョブの画面で Erase ボタンを押せば手動で消せる。artifact は上記の通り有効期限を付けて自動で消すこともできるけど、ビルドログは Erase ボタンで消さない限り消えなさそう。

Erase ボタンでもビルドログや成果物が消えるだけで、ビルドやパイプラインの履歴そのものは消せなさそう。一時的にパイプラインを無効→有効としても消えたりはしない。プロジェクトを削除すれば・・どうなるかは試していない。

Jenkins だとビルド履歴を世代数や日数で削除できるので、それができないことに違和感が無くもないけど、そもそもバージョン管理システムとはすべての履歴を残すためのものなんだから、ビルドの履歴が残るのもそういうものだと言われればそういうものな気もする。

強いて言えば試行錯誤した履歴まで残るのがちょっとかっこ悪い感じする。

アーティファクトのメモ

artifact、(この界隈では)日本語で成果物と訳されると思うけど、アーティファクトとカタカナで書くと、なんか、すごそう、すごい力を秘めてそうで違和感。

Gitlab CI を使ってみるメモ

Jenkins からの移行のために今更だけど使ってみたメモ。

なお、うちの Gitlab はソースから入れていてデータベースも MySQL です。たまにしかバージョンアップしていないのでちょっと古いです(8.17.2)。

参考

ざっくり

  • Gitlab 8.0 からは Gitlab に統合されている
  • Runner をどこかのサーバでセットアップする必要がある
    • shell とか docker とかでビルドが実行される環境
    • Gitlab と同じサーバじゃ無い方が良い
  • リポジトリルートに .gitlab-ci.yml を追加する
    • .gitlab-ci.yml は Runner に何をさせるかを記述する
  • どの Runner を使うかはプロジェクトごとに選択できる
    • タグ付けでさらに細かく制御できる

Runner とは

ビルドを実行するための環境。公式の実装として gitlab-ci-multi-runner があるけれども自前で実装することもできます、たぶん。

起動すると Gitlab に HTTP で常時接続して Gitlab からのビルドの通知を受け取ります。なので「Runner → Gitlab」の方向に HTTP で繋がるだけで良いです。

Runner には、特定のプロジェクトに固有のものと(Specific Runner)、すべてのプロジェクトで使用可能なものがあります(Shared Runner)。

Shared Runner は fair usage queue という方法でジョブをキューイングします。一方で Specific Runner は FIFO でキューイングされます。fair usage queue は、やたらたくさんビルドを要求するプロジェクトが原因で他のプロジェクトのビルドが滞るのを避けるためのアルゴリズムのようです。

Specific Runner は複数のプロジェクトで使いまわすこともできます。Shared Runner との違いはそれぞれのプロジェクトで個別に有効にする必要があるかどうかです。Shared Runner はデフォですべてのプロジェクトで有効になります。Specific Runner はプロジェクトの設定で選択しなければ有効になりません。Specific Runner は使い回しを禁止して特定のプロジェクトにロックすることもできます。

Shared Runner を登録するとき、普通は Runner に処理可能なジョブのタグを指定します。そうしないとすべてのジョブを実行しようとしてしまうので。もちろん Specific Runner でも特定の環境でだけ実行するジョブのためにタグを指定しても良い。

Shared Runner でジョブを実行する場合、同じ Runner で実行される他のプロジェクトのコードにアクセスできるので注意が必要。また、Runner のトークンが実行するコードで取得できるので、Runner のクローンを作成して誤ったジョブをサブミットできる??(Runner に間違ったビルドを支持できるということ? もしくは Runner を上書きできる? あるいは 任意の Runner が追加できてしまうということ?)

Runner のインストール

最新版だと今使ってる Gitlab とバージョンが合わなかったのでダウングレードします(最初からバージョン指定でインストールしても良い)。

curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-ci-multi-runner/script.rpm.sh |
  sudo bash

sudo yum install gitlab-ci-multi-runner
sudo yum downgrade gitlab-ci-multi-runner-1.11.4

rpm -ql gitlab-ci-multi-runner
#=> /usr/bin/gitlab-ci-multi-runner
#=> /usr/bin/gitlab-runner
#=> /usr/share/gitlab-runner/clear-docker-cache
#=> /usr/share/gitlab-runner/post-install
#=> /usr/share/gitlab-runner/pre-remove

RPM に systemd のユニットファイルとかは含まれていませんが、インストール時に /usr/share/gitlab-runner/post-install で生成されているようなのですぐ開始できます(というかインストールした時点で開始されているっぽいですが)。

sudo systemctl start gitlab-runner.service
sudo systemctl status gitlab-runner.service
sudo systemctl enable gitlab-runner.service

Runner を Gitlab に登録

特定のプロジェクト用に Runner を登録してみる。

プロジェクトの CI/CD Pipelines を開いて Specific Runners のとこの URL とトークンをメモって、下記のコマンドで登録します。

sudo gitlab-runner register \
    --url http://gitlab.example.net/ci \
    --registration-token "$gitlab_ci_token" \
    --name ore-no-shell \
    --executor shell

Enter 連打で登録できる。Executor で shell を指定しているのでビルドはこのホスト上の gitlab-runner というユーザーを使ってそのままスクリプトが実行されます。

Docker の Runner も登録してみる。

sudo gitlab-runner register \
    --url http://gitlab.example.net/ci \
    --registration-token "$gitlab_ci_token" \
    --name ore-no-docker \
    --tag-list docker \
    --executor docker \
    --docker-image alpine:latest

タグを指定したのでこの Runner では docker というタグが付けられたジョブだけが実行されます。また、指定している Docker image は .gitlab-ci.yml で指定されなかったときのデフォルトになります。.gitlab-ci.yml で指定できるイメージのホワイトリストとかも指定できるようですね。

.gitlab-ci.yml

.gitlab-ci.yml をリポジトリルートに追加する。このファイルに Runner が何をするか記述します。

image: php:alpine

before_script:
  - uname -n
  - id
  - pwd
  - which php
  - php -v

job_shell:
  script:
    - echo "this is shell"

job_docker:
  script:
    - echo "this is docker"
  tags:
    - docker

before_script はすべてのジョブに先立って実行されます。job_shelljob_docker がジョブで任意の名前を付けることができる。ジョブの子要素には script が必須。

このファイルをリポジトリに追加してプッシュすると次のようにビルドが実行されます(見やすくするために少し編集)。

job_shell

Running with gitlab-ci-multi-runner 1.11.4 (7e2b646)
  on ore-no-shell (9b476e4f)
WARNING: image is not supported by selected executor and shell
Using Shell executor...
Running on ore.example.com...
Fetching changes...
HEAD is now at e20b44a .gitlab-ci.yml
From http://gitlab.example.net/ore/testing
   e20b44a..fc613e6  master     -> origin/master
Checking out fc613e6d as master...
Skipping Git submodules setup

$ uname -n
ore.example.com

$ id
uid=983(gitlab-runner) gid=981(gitlab-runner) groups=981(gitlab-runner)

$ pwd
/home/gitlab-runner/builds/9b476e4f/0/ore/testing

$ which php
/usr/bin/php

$ php -v
PHP 7.1.6 (cli) (built: Jun  7 2017 12:15:54) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
    with Zend OPcache v7.1.6, Copyright (c) 1999-2017, by Zend Technologies
    with Xdebug v2.5.5, Copyright (c) 2002-2017, by Derick Rethans

$ echo "this is shell"
this is shell

Job succeeded

job_docker

Running with gitlab-ci-multi-runner 1.11.4 (7e2b646)
  on ore-no-docker (adea055c)
Using Docker executor with image php:alpine ...
Pulling docker image php:alpine ...
Running on runner-adea055c-project-42-concurrent-0 via ore.example.com...
Cloning repository...
Cloning into '/builds/ore/testing'...
Checking out fc613e6d as master...
Skipping Git submodules setup

$ uname -n
runner-adea055c-project-42-concurrent-0

$ id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

$ pwd
/builds/ore/testing

$ which php
/usr/local/bin/php

$ php -v
PHP 7.1.6 (cli) (built: Jun 28 2017 20:57:42) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies

$ echo "this is docker"
this is docker

Job succeeded

CI Lint

Gitlab の Pipelines のページから CI Lint に移動できます。その画面で .gitlab-ci.yml の検証ができる。

Docker cache

Docker でジョブを実行するとき、composer install で毎回パッケージがダウンロードされたのではビルドが遅くなりすぎて辛いので、ビルドの終了時に一部のディレクトリをキャッシュして次回のビルドで使うようにできます。

.gitlab-ci.yml でキャッシュするディレクトリを指定します。

image: php:alpine

phpunit:
  script:
    - php composer.phar install --prefer-dist --no-progress --ansi --dev
  tags:
    - docker
  cache:
    paths:
      - vendor/

ビルドの終了時に指定していたディレクトリがアーカイブされて保存されます。次回の実行時はそのアーカイブが展開してからビルドが開始します。

ちなみにアーカイブは自動的に作成された Docker Volume に保存されています。docker volume ls で見れるアレです。

下記のように登録時に --docker-cache-dir を指定するとホストのディレクトリが使われるので Docker Volume がぼこぼこ作成されることはなくなります。

sudo gitlab-runner unregister \
    --url http://gitlab.example.net/ci \
    --name ore-no-docker

sudo gitlab-runner register \
    --url http://gitlab.example.net/ci \
    --registration-token "$gitlab_ci_token" \
    --name ore-no-docker \
    --tag-list docker \
    --executor docker \
    --docker-image alpine:latest \
    --docker-cache-dir "/srv/gitlab-runner/cache/"

特定の処理だけ実行する Runner を作る

gitlab-runner--pre-build-script でスクリプトを指定すると、この Runner に固有のビルド前の処理が実行できます。

さらに、指定したスクリプトはビルドのジョブの script と繋げられて1つのスクリプトとして実行されるので、次のように exec とかしてやるとジョブの script が実行されずにジョブが終了します。

sudo gitlab-runner register \
    --url http://gitlab.example.net/ci \
    --registration-token "$gitlab_ci_token" \
    --name ore-no-task \
    --tag-list ore-no-task \
    --executor shell \
    --builds-dir /srv/gitlab-runner/builds \
    --pre-build-script 'exec /opt/gitlab-runner/pre-build-script.sh'

sudo mkdir -p /opt/gitlab-runner/

cat <<'EOS'| sudo tee /opt/gitlab-runner/pre-build-script.sh
#!/bin/bash
set -x
id
pwd
echo "CI_PROJECT_PATH=$CI_PROJECT_PATH"
git show -s
EOS

sudo chmod +x /opt/gitlab-runner/pre-build-script.sh

こんな感じに実行されます。.gitlab-ci.ymlscript に何を書いていてもここで止まります。

Running with gitlab-ci-multi-runner 1.10.8 (2c34bd0)
Using Shell executor...
Running on ore.example.com...
Fetching changes...
HEAD is now at e20b44a .gitlab-ci.yml
Checking out e20b44a6 as master...
Skipping Git submodules setup

$ exec /opt/gitlab-runner/pre-build-script.sh

+ id
uid=983(gitlab-runner) gid=981(gitlab-runner) groups=981(gitlab-runner)

+ pwd
/srv/gitlab-runner/builds/ce74c254/0/ore/testing

+ echo CI_PROJECT_PATH=ore/testing
CI_PROJECT_PATH=ore/testing

+ git show -s
commit e20b44a678298d9280ce0f6e90128512c078a955
Author: ore <ore@example.com>
Date:   Fri Jun 30 12:22:44 2017 +0900

    .gitlab-ci.yml

Build succeeded

複数のプロジェクトで横断的に特定のホストである決まった処理を実行するために使えそうです。

Jenkins を置き換える

今は Jenkins で下記のような感じで CI/CD してます。

  • 開発環境を Jenkins Slave としてセットアップ
    • 世間ではステージングと呼ばれるかも
  • リポジトリにプッシュされたらテストを実行
    • composer install とか
    • migration とか
    • phpunit とか
    • php-cs-fixer とか
    • phan とかもやりたい
  • master ブランチでテストが通れば開発環境の公開ディレクトリへデプロイ
    • Jenkins のワーキングディレクトリを公開ディレクトリに指定してる
    • ので Jenkins がファイルを撒くまでやってくれる(チェックアウトするだけ)
    • マイグレーションとかサービスのリスタートとかだけ後処理でやってる
  • さらに master ブランチを Redmine にチェックアウトして fetch changesets で反映
    • Redmine も Jenkins Slave として登録している
    • トピックブランチは Redmine に反映したくない

これらを Jenkins の WebUI で設定するのは流石にしんどいので Jenkins DSL で設定しているのですが・・それはそれでツラミあります。あと、現状のビルドの一部を並列化して両方終わったらデプロイを実行、とかが Jenkins DSL だとかなりつらいです。Jenkins Pipeline で多少マシになっているっぽいですが。。。

Gitlab CI ならこんな感じでできそう(phan はそれっぽい Docker Image を作った)。

before_script:
  - curl -fsSL https://getcomposer.org/download/1.4.2/composer.phar > composer.phar

phan:
  image: ngyuki/php-phan
  script:
    - php composer.phar install --prefer-dist --no-progress --ansi --no-dev
    - phan --version
    - phan -l src/ -l vendor/ -3 vendor/
  tags:
    - docker
  cache:
    paths:
      - vendor/

cs-fixer:
  image: php:alpine
  script:
    - php composer.phar install --prefer-dist --no-progress --ansi --dev
    - vendor/bin/php-cs-fixer fix --dry-run --diff --ansi -vvv -- src/
  tags:
    - docker
  cache:
    key: vendor
    paths:
      - vendor/

phpunit:
  image: php:alpine
  script:
    - php composer.phar install --prefer-dist --no-progress --ansi --dev
    - vendor/bin/phpunit
  tags:
    - docker
  cache:
    key: vendor
    paths:
      - vendor/

deploy:
  stage: deploy
  script:
    - php composer.phar install --prefer-dist --no-progress --ansi --no-dev --optimize-autoloader
    - ln -sfn -- "$PWD" /opt/myapp/current
  only:
    - master
  tags:
    - dev

redmine:
  stage: deploy
  script: |
    set -eu
    mkdir -p -- "/srv/gitlab-runner/repos/${CI_PROJECT_PATH%/*}"
    ln -sfn -- "$PWD/.git" "/srv/gitlab-runner/repos/${CI_PROJECT_PATH}"
    git branch --force -- "$CI_BUILD_REF_NAME" "$CI_BUILD_REF"
    git symbolic-ref HEAD "refs/heads/$CI_BUILD_REF_NAME"
    curl -kfs "https://redmine.example.net/sys/fetch_changesets?id=ore"
  only:
    - master
  tags:
    - redmine

最後の redmine のジョブは gitlab-runner--pre-build-script を使って Shared Runner でやると良さそう。

特定のユーザーで Runner を実行する

Runner がどのユーザーで実行されるかは gitlab-runner run の引数で決まるのだけど、これは systemd のユニットファイルで指定されている。

cat /etc/systemd/system/gitlab-runner.service  | grep ExecStart
#=> ExecStart=/usr/bin/gitlab-ci-multi-runner "run" ... "--user" "gitlab-runner"

このファイルは gitlab-runner install で生成されるので、下記のようにすれば別のユーザーで実行させることができる。

gitlab-runner uninstall
gitlab-runner install -d /tmp -u ore
systemctl restart gitlab-runner

なお gitlab-runner register で登録する Runner ごとには変更できないっぽい。

debian ベースの Docker コンテナで busybox の cron を実行

DockerHub の言語系のイメージは(alpine のもあるけれども)debian ベースのものが多いですが cron でスクリプトを定期的に実行しようとして、

RUN apt-get update && apt-get install -y cron

とかすると、

  • cron から実行するスクリプトにコンテナの環境変数が渡されない
    • コンテナの開始時に環境変数をファイルに書いて cron から実行するスクリプトで読んだり
  • スクリプトの標準出力や標準エラーがどこに行くのかよくわからない
    • スクリプトはログをファイルに出力して tail -f したり
    • そのログだれがローテートするの?

とか、とてもつらそうです。

ので、cron の代わりに michaloo/go-cron を使ってみたりしてたのですが、下記の記事を見まして、

もしかして busybox の crond ならそういう諸々の問題は無いのでは? と思って試しました。

Dockerfile

FROM php:7.1-cli

RUN apt-get update && apt-get -y install busybox-static

COPY crontab /var/spool/cron/crontabs/root
COPY script.php /app/script.php

CMD busybox crond -l 2 -L /dev/stderr -f

crontab

* * * * * php /app/script.php

script.php

<?php
system('env | sort | grep OREORE');
fprintf(STDERR, "%s: this is stderr\n", date('H:i:s'));

ビルドして実行します。

docker build . -t oreore/example-php-cron
docker run --name=oreore -e OREORE=12345 --rm oreore/example-php-cron

環境変数 OREORE=12345 がスクリプトに渡って、かつ、スクリプトの標準出力や標準エラーがそのまま Docker に伝わっています。

crond: crond (busybox 1.22.1) started, log level 2
crond: USER root pid   6 cmd php /app/script.php
OREORE=12345
10:50:05: this is stderr
crond: USER root pid  12 cmd php /app/script.php
OREORE=12345
10:51:01: this is stderr

Cacti でアラートを thold プラグインを使わずにやるアイデア(中途半端)

Cacti の thold プラグインでアラートを仕込むのがめんどくさくて仕方なかったので、もういっそのこと直接 rra ファイルを見てアラートを出すスクリプトを自前で作れば良いんじゃないかと思ったときのメモ。

Cacti のバージョンは 0.8.8h です。

Prometheus に入門したので中途半端なままで放置。


rra ファイルのパスは cacti の DB から下記のような SQL で取ります。

SELECT
  host.id as host_id,
  host.description as host_description,
  data_template.name as data_template_name,
  data_template_rrd.data_source_name as data_source_name,
  poller_item.rrd_path as rrd_path
FROM data_local
INNER JOIN host
  ON data_local.host_id = host.id
INNER JOIN data_template
  ON data_local.data_template_id = data_template.id
INNER JOIN data_template_rrd
  ON  data_local.id = data_template_rrd.local_data_id
  AND data_local.data_template_id = data_template_rrd.data_template_id
INNER JOIN poller_item
    ON data_local.id = poller_item.local_data_id

メトリクスの値は rrdtool で下記のように取れます。

rrdtool fetch hoge_prod_ap01_load_1min_100.rrd MAX \
    -s $(( $(date +%s) - 3600 )) -e $(( $(date +%s) - 300 ))
                      load_1min

1490619600: 7.0000000000e-02
1490619900: 8.6600000000e-02
1490620200: 6.4900000000e-02
1490620500: 2.6266666667e-02
1490620800: 3.3333333333e-03
1490621100: 6.6933333333e-02
1490621400: 7.1633333333e-02
1490621700: 8.6400000000e-02
1490622000: 9.0000000000e-02
1490622300: 1.5533333333e-01
1490622600: 5.7533333333e-02
1490622900: 3.0000000000e-02

あるいは xport なら複数の rra から JSON で取得できるので、こっちのが良さそう。

rrdtool xport --json \
    -s $(( $(date +%s) - 3600 )) -e $(( $(date +%s) - 300 )) \
    DEF:mem_buffers=hoge_prod_ap01_mem_buffers_101.rrd:mem_buffers:MAX \
    DEF:mem_cache=hoge_prod_ap01_mem_cache_102.rrd:mem_cache:MAX \
    DEF:mem_free=hoge_prod_ap01_mem_free_103.rrd:mem_free:MAX \
    DEF:mem_total=hoge_prod_ap01_mem_total_104.rrd:mem_total:MAX \
    XPORT:mem_buffers:mem_buffers \
    XPORT:mem_cache:mem_cache \
    XPORT:mem_free:mem_free \
    XPORT:mem_total:mem_total
{ about: 'RRDtool xport JSON output',
  meta: {
    "start": 1490619900,
    "step": 300,
    "end": 1490619900,
    "legend": [
      'mem_buffers',
      'mem_cache',
      'mem_free',
      'mem_total'
          ]
     },
  "data": [
    [ 8.5200000000e+02, 1.9999189200e+06, 7.8921592000e+05, 3.8821600000e+06 ],
    [ 8.5200000000e+02, 2.0017213467e+06, 7.8882005333e+05, 3.8821600000e+06 ],
    [ 8.5200000000e+02, 2.0034699067e+06, 7.8495132000e+05, 3.8821600000e+06 ],
    [ 8.5200000000e+02, 2.0018100000e+06, 7.8899000000e+05, 3.8821600000e+06 ],
    [ 8.5200000000e+02, 2.0028664133e+06, 7.8575062667e+05, 3.8821600000e+06 ],
    [ 8.5200000000e+02, 2.0050689867e+06, 7.8603305333e+05, 3.8821600000e+06 ],
    [ 8.5200000000e+02, 2.0068675200e+06, 7.8260720000e+05, 3.8821600000e+06 ],
    [ 8.5200000000e+02, 2.0052690000e+06, 7.8560060000e+05, 3.8821600000e+06 ],
    [ 8.5200000000e+02, 2.0062675333e+06, 7.8268226667e+05, 3.8821600000e+06 ],
    [ 8.5200000000e+02, 2.0079922267e+06, 7.8281285333e+05, 3.8821600000e+06 ],
    [ 8.5200000000e+02, 2.0063401600e+06, 7.8266224000e+05, 3.8821600000e+06 ],
    [ 8.5200000000e+02, 2.0078709467e+06, 7.8317032000e+05, 3.8821600000e+06  ]
  ]
}

がしかし、これよく見たら JSON としては invalid ですね。。。--json を指定しなければ XML なのでそっちの方が良いかも。

あとは・・・

  • ホスト名は host.description に対して正規表現でパターンで指定する
    • それっぽく description が設定されている前提
    • <service>-<env>-<role><num> みたいに
  • data_source_name だけではメトリクスを一意に特定できないことがあるのでデータテンプレートも指定する
    • データテンプレートは名前でしか識別できなさそうなのでエイリアスを正規表現で指定する
    • 実際の閾値設定ではエイリアス名で指定する
    • data_source_name だけで一意になるなら監視設定ではデータテンプレートのエイリアスは省略しても良い
  • メトリクスをコールバックで加工した値に閾値を設定する

例えば下記のように設定する。

return [
    // データテンプレートのエイリアス
    // 複数のデータテンプレートがマッチするがデータソース名と足して一意になる前提
    'alias_data_template' => [
        'memory' => '^ucd/net - Memory -',
    ],
    'alert' => [
        [
            // ホスト名のパターン
            'host' => '^hoge-prod-ap\d+$',

            // データテンプレートのエイリアス+データソースのリスト
            'ds' => [
                'memory:mem_total',
                'memory:mem_buffers',
                'memory:mem_cache',
                'memory:mem_free',
            ],
            // チェックする値、引数の並びは ds に対応
            'expr' => function ($total, $buffer, $cache, $free) {
                return ($total - $buffer - $cache - $free) / $total * 100;
            },
            // 閾値
            'threshold' => [
                'WARNING' => 80,
                'CRITICAL' => 90,
            ],
            // 通知先
            'notify' => [
                'WARNING' => ['ore@example.com', 'are@example.com'],
                'CRITICAL' => 'sore@example.com',
            ],
            // 通知のメッセージ
            'message' => '{severity} [{hostname}] memory usage too high ... now:{value} > {threshold}',
        ],
    ],
];

なにかしらスクリプトを実行することで、DB を舐めてホスト名やデータテンプレート名のパターンとデータソースに基づく rra のパスをキャッシュしておくことで、定期的な閾値のチェック処理を軽減する。

例えば下記のようなキャッシュになるだろうか。

return [
    'host_ids' => [
        // パターンに対するホストIDの一覧
        '^hoge-prod-ap\d+$' => [1, 2, 3, 4],
    ],
    'rra_paths' => [
        // ホストID => データテンプレート+データソース => RRA
        1 => [
            'memory:mem_total'   => '/path/to/rra/are_prod_ap01_mem_total_1.rra',
            'memory:mem_buffers' => '/path/to/rra/are_prod_ap01_mem_buffers_2.rra',
            'memory:mem_cache'   => '/path/to/rra/are_prod_ap01_mem_cache_3.rra',
            'memory:mem_free'    => '/path/to/rra/are_prod_ap01_mem_free_4.rra',
        ],
    ],

];

Prometheus を使ってみた感じ、さくさく設定できてもうこれでいいかなと思ったので、この案はお蔵入り。

PhpStorm の Run/Debug で docker-compose run でテストを実行

docker-compose で複数のコンテナで構成されている環境に対して、PhpStorm の Run/Debug で docker-compose run でテストなどを実行できるようにしたときのメモ。


PhpStorm 2016.1 ぐらいから Remote interpreter に Docker が追加されており、PhpStorm の Run/Debug で「コンテナ作成&スクリプトやテスト実行」ができるようになっています。

Xdebug を実行する PHP のイメージにインストールしておけばデバッガでアタッチすることもでき、非常に便利そうですが・・・

単一のイメージから単一のコンテナを作成して実行するだけなので、docker-compose などで複数のコンテナ(DBとか)を実行する構成だと Run/Debug でテストを実行したときに DB が無くてテストがコケます。

docker run ではなく docker-compose run できれば解決なのだけれども。。。

いずれ出来るようになりそうな気がするけど、当座の方法として Vagrant の Remote interpreter で php executable を docker-compose run で php のコンテナを実行するシェルスクリプトに差し替えることで対応しました。

方法

下記のように設定します。

  • Vagrant 環境に docker-engine と docker-compose をインストール
    • docker は vagrant ユーザーから実行できるようにする
  • docker-compose.yml で下記をマウントするように設定
    • $PWD:$PWD:rw
      • /vagrant:/vagrant:rw とかでも ./:/vagrant:rw とかでも良い
    • $HOME:$HOME:ro
      • /home/vagrant:/home/vagrant:ro とかでも良い
  • Remote interpreter の php executable で後述のスクリプトを指定する

Vagrantfile で synced_folderssh.username を変更したときのことを考慮して $PWD とか $HOME とか使っているけどベタに書いても構いません。

php executable に指定するスクリプトは下記の内容で、プロジェクトルートに配置します。

#!/bin/bash

cd -- "$(dirname -- "$(readlink -f -- "$0")")" # `cd /vagrant` とかでも良い

exec docker-compose run --rm -T -w "$OLDPWD" \
  -e SSH_CLIENT="$SSH_CLIENT" -e XDEBUG_CONFIG="$XDEBUG_CONFIG" php php "$@"

2つある php の1つめは docker-compose はサービスの名前なので、適宜変更のこと。

後は Vagrant 環境の中で docker-compose up でコンテナを開始し、PhpStorm の Run/Debug でテストなどを実行すると docker-compose run で実行されます。

php のイメージに xdegug をインストールしておけばブレークポイントを仕込んで止めたり出来ます。

試行錯誤のメモ

プロジェクトルートのディレクトリ

コンテナ内でのプロジェクトルートのディレクトリは Vagrant 環境でのディレクトリと同じディレクトリになる必要があります。 そうしないと、リモートデバッグ時にコンテナ内の xdebug から通知されるパスと、ローカルの実際のパスの対応がとれません。

普通は /vagrant:/vagrant:rw とかだけど、Vagrantfile で、

config.vm.synced_folder ".", "/app"

のように変更しているなら /app:/app:rw とかに変更します。

このディレクトリは docker-compose 実行時のカレントディレクトリになるはずなので $PWD:$PWD:rw とかでも良いです。↑ではそうしてます。

Vagrant ユーザーのホームディレクトリ

PhpStorm の Vagrant の Remote inspector は Vagrant のログインユーザーのホームディレクトリに .phpstorm_helpers/phpinfo.php というファイルを配置し、これを実行することで環境の情報を得ます。

ので、このファイルがホスト(Vagrant 環境)とコンテナで共有されている必要があります。

↑ではホームディレクトリまるごとを共有しています。

SSH_CLIENT 環境変数

xdebug.remote_host の値の算出のために SSH_CLIENT 環境変数 が使用されるので、ホストの SSH_CLIENT 環境変数をコンテナにそのまま渡す必要があります。

XDEBUG_CONFIG 環境変数

リモートデバッグの開始のために XDEBUG_CONFIG 環境変数が必要なので、ホストの XDEBUG_CONFIG 環境変数をコンテナにそのまま渡す必要があります。

Local interpreter ではダメ?

Vagrant を噛まさずに Local の interpreter で php executable を↑のようなスクリプトにすれば、Docker for Mac で同じことが実現できるかと思ったけれども、xdebug.remote_host が 127.0.0.1 固定になるようで、コンテナから 127.0.0.1 ではホストにアクセスできないのでダメでした。

また、そもそも Docker for Windows だとパスの対応が取れなくてダメだと思います(C:\Users\ore -> /C/Users/ore とか)。

今回は Mac で試しているのだけど Windows でも出来るようにしたいので Vagrant のが無難だろうと思います。

スクリーンショット

f:id:ngyuki:20170326102634p:plain

f:id:ngyuki:20170326102646p:plain

f:id:ngyuki:20170326102652p:plain