読者です 読者をやめる 読者になる 読者になる

tkuchikiの日記

Linux やプログラミングについて書きます。

WEB+DB PRESS Vol.94 で特集「実践スケーラブルAWS」を執筆しました & 恵贈御礼

AWS WEB+DB PRESS

本日 8/24 発売の WEB+DB PRESS Vol.94 で、 特集1「[鍵は監視にあり!]実践スケーラブルAWS 規模に適した設計,負荷に応じた増減,障害への自動対応」
の第3章から5章を執筆しました。

gihyo.jp

謝辞

株式会社技術評論社 WEB+DB PRESS編集部様からご恵贈いただきました。ありがとうございます。
また、この度は執筆の機会をいただきましたこと重ねて御礼申し上げます。

もともと、fujiwara さん宛てに執筆依頼があったのですが、
無理を言って共著で書かせていただくことになりました。機会をいただきありがとうございました。

f:id:tkuchiki:20160819201510j:plain

特集「実践スケーラブル AWS」について

この特集では、AWS で1〜100台規模のサーバを運用する際、どのようなことを考えて設計するとスケールしやすくなるのか解説しています。
今まで WEB+DB PRESS またはその他の雑誌で、数多くの AWS の特集が組まれてきたと思いますが、
本特集ではタイトルにもあるように「監視」に重点を置いた内容となっております。
第2章では運用における監視についてやどのような監視が必要かを Zabbix と Mackerel を例に紹介しています。第3章ではWeb・アプリケーションサーバ、第4章ではキャッシュサーバ(memcached と Redis)、第5章ではデータベースサーバ(MySQL) の監視について記しております。第6章では、オートスケールの監視について触れています。特に3〜5章では、各章2ページほど監視について紙面を割いています。
全体を通して、まだAWSでの監視やオートスケールに取り組んでいない方もすでにがっつり運用されている方にも参考になる内容になっているのではないかと思います(そうであれば幸いです)。

特集は32ページありますので、読み応えがあると思います。
是非、お手にとっていただけますと幸いです!

HAProxy の external-check で Read Replica から Master に昇格した Amazon Aurora に接続できないようにする

haproxy

追記 2016/07/22 19:06

error: 'Lost connection to MySQL server at 'reading initial communication packet', system error: 0' というエラーがでて接続できなくなる現象が発生するようなので、改良が必要なようです。
エラーの検証をしようとしたら再現できなかったので、とりあえず気にしなくても良さそうです。

追記 2016/07/19 18:48

ブックマークにもコメントいただきましたが、
SHOW GLOBAL VARIABLES LIKE 'innodb_read_only'; を実行して、
OFF なら master、ON なら Read Replica という判断方法もあります。
Amazon Aurora を使用する際のベストプラクティス に書いてあります。
なぜ、これを使わなかったかというと、検証したときは知らなかったからです...

検証した環境は以下の通りです。

$ haproxy -v
HA-Proxy version 1.6.6 2016/06/26
Copyright 2000-2016 Willy Tarreau <willy@haproxy.org>

$  cat /etc/system-release
Amazon Linux AMI release 2016.03

RDS for MySQL は Multi-AZ 構成にした場合、スタンバイ用のインスタンスにアクセスできません。
Amazon Aurora(以降、Aurora) はスタンバイインスタンスが Read Replica となっているため参照することができます。
Aurora を HAProxy で参照分散するとき、Read Replica のいずれかが Master に昇格するため、
Read Replica のつもりで参照していたインスタンスが Master になっている可能性があります。
Slave 参照しない Read Replica を用意し、Master に昇格させるインスタンスの優先順位を調整することで、 HAProxy からは Master に参照することがないように調整することも可能ですが、
一台は余剰なインスタンスとなってしまいますし、3台構成の場合に Master と Read Replica 1台が同時にダウンした場合どうするかなど、 考慮しなくてはならないことがたくさんあります。
もちろん、Master を参照しても負荷的に問題なければ良いですが、Slave 参照する場合は Master への負荷を軽減したい場合でしょうから問題になるケースが多いのではないでしょうか。
以上のことから、HAProxy の mysql-check では意図しない挙動をしてしまいます。 そこで、HAProxy の external-check という、任意のコマンドでヘルスチェックを行う機能を利用できないか検証しました。

haproxy.conf

使用した設定ファイルです。

global
   external-check

resolvers mydns
   nameserver dns1 10.2.0.2:53
   nameserver dns2 8.8.8.8:53
   resolve_retries 3
   timeout retry   1s
   hold valid      60s

defaults
    log             127.0.0.1 local1 debug
    retries 2
    timeout connect 3000
    timeout server 5000
    timeout client 5000

frontend f_mysql_slave
    bind            127.0.0.1:3307
    default_backend b_mysql_slave

backend b_mysql_slave
    log             127.0.0.1 local1 debug
    mode tcp
    option external-check
    external-check path "usr/bin:/bin:/usr/local/bin"
    external-check command /usr/local/bin/check_mysql_slave.sh
    balance         roundrobin
    server          slave test-db01.xxxxxx.ap-northeast-1.rds.amazonaws.com:3306 check resolvers mydns
    server          master test-db.cluster-xxxxxx.ap-northeast-1.rds.amazonaws.com:3306 check resolvers mydns backup

external-check を使うためには、

global
   external-check

backend b_mysql_slave
        option external-check
        external-check path "usr/bin:/bin:/usr/local/bin"
        external-check command /usr/local/bin/check_mysql_slave.sh

が必要です。

server master ... backup は Read Replica が全部ダウンしたときのみ Master を参照するための設定です。

external-check command

HAProxy がコマンドを実行する際に環境変数を設定するようになっています。
環境変数は以下のとおりです。

  • HAPROXY_PROXY_ADDR
    • backend の bind IP
    • backend に定義した bind は無視されるので実質使用しない(はず)
  • HAPROXY_PROXY_ID
    • backend ID.
  • HAPROXY_PROXY_NAME
    • backend 名
  • HAPROXY_PROXY_PORT
    • backend の bind port
    • backend に定義した bind は無視されるので実質使用しない(はず)
  • HAPROXY_SERVER_ADDR
    • server の IP
  • HAPROXY_SERVER_CURCONN
    • server のカレントコネクション数
  • HAPROXY_SERVER_ID
    • server の ID
  • HAPROXY_SERVER_MAXCONN
    • server の 最大コネクション数
  • HAPROXY_SERVER_NAME
    • server の設定名
  • HAPROXY_SERVER_PORT
    • server で設定したホストの port
  • PATH
    • external-check path で設定した PATH

詳細は external-check command を参照してください。 環境変数を上から順に出力した例は以下のとおりです。

 
2
b_mysql_slave

10.206.1.20
0
2
0
slave
3306
usr/bin:/bin:/usr/local/bin

check_mysql_slave.sh

ヘルスチェックを行うスクリプトです。

#!/bin/bash

BACKUP="master"
MASTER_IP=$(dig +short test-db.cluster-cnttkfxtxwao.ap-northeast-1.rds.amazonaws.com | tail -n 1)
[ "${MASTER_IP}" = "" ] && exit 1
[ "${HAPROXY_SERVER_NAME}" = "${BACKUP}" ] && exit 0
[ "${HAPROXY_SERVER_ADDR}" != "${MASTER_IP}" ]
  • ${MASTER_IP} が取得できなければ fail
  • ${HAPROXY_SERVER_NAME} = ${BACKUP} の場合、backup 以外のインスタンスはダウンしているので Master を参照するようにする
  • ${HAPROXY_SERVER_ADDR} != ${MASTER_IP} の場合、Read Replica を参照しているのでヘルスチェックを success にする
追記: 2016-07-25 16:05

SHOW GLOBAL VARIABLES LIKE 'innodb_read_only'; を使う場合は以下のようになります。

#!/bin/bash

BACKUP="master"
READ_ONLY=$(mysql -u haproxy -h ${HAPROXY_SERVER_ADDR} -P ${HAPROXY_SERVER_PORT} -BN -e "SHOW GLOBAL VARIABLES LIKE 'innodb_read_only';" --connect-timeout=5 | awk '{print $2}')

[ "${HAPROXY_SERVER_NAME}" = "${BACKUP}" ] && exit 0
[ "${READ_ONLY}" = "ON" ]

DNSを引く場合と違って、BACKUP 以外は環境ごとに変わる要素がなくなったので使いやすくなっています。

検証結果

Master と Read Replica それぞれ 1 台ずつで 2 種類の検証を行いました。

Read Replica が Master に昇格した場合

backup の設定は削除した状態です。
server slave = Master なのでヘルスチェックに失敗しています。
Read Replica が 1 台なので no server available になっていますが、Read Replica が 2台以上ある場合は、
Read Replica には接続できて、Master には接続できない状態になります。

Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1032ms, status: 2/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1032ms, status: 2/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1007ms, status: 1/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1007ms, status: 1/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1016ms, status: 0/2 DOWN.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1016ms, status: 0/2 DOWN.
Server b_mysql_slave/slave is DOWN. 0 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.
Server b_mysql_slave/slave is DOWN. 0 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.
backend b_mysql_slave has no server available!
backend b_mysql_slave has no server available!

backup を設定して、Read Replica が Master に昇格した場合

backup 以外の server のヘルスチェックに失敗して、backup のみヘルスチェックに成功した場合のログです。
Master 以外ダウンしているときは Master に接続できるようになっていることがわかります。

Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1014ms, status: 2/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1014ms, status: 2/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1007ms, status: 1/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1007ms, status: 1/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1014ms, status: 0/2 DOWN.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1014ms, status: 0/2 DOWN.
Server b_mysql_slave/slave is DOWN. 0 active and 1 backup servers left. Running on backup. 0 sessions active, 0 requeued, 0 remaining in queue.
Server b_mysql_slave/slave is DOWN. 0 active and 1 backup servers left. Running on backup. 0 sessions active, 0 requeued, 0 remaining in queue.
b_mysql_slave/master changed its IP from 10.2.0.10 to 10.2.1.20 by mydns/dns1.
b_mysql_slave/master changed its IP from 10.2.0.10 to 10.2.1.20 by mydns/dns1.
Health check for server b_mysql_slave/slave succeeded, reason: External check passed, code: 0, check duration: 1015ms, status: 1/2 DOWN.
Health check for server b_mysql_slave/slave succeeded, reason: External check passed, code: 0, check duration: 1015ms, status: 1/2 DOWN.
Health check for server b_mysql_slave/slave succeeded, reason: External check passed, code: 0, check duration: 1007ms, status: 3/3 UP.
Health check for server b_mysql_slave/slave succeeded, reason: External check passed, code: 0, check duration: 1007ms, status: 3/3 UP.
Server b_mysql_slave/slave is UP. 1 active and 1 backup servers online. 0 sessions requeued, 0 total in queue.
Server b_mysql_slave/slave is UP. 1 active and 1 backup servers online. 0 sessions requeued, 0 total in queue.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1014ms, status: 2/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1014ms, status: 2/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1007ms, status: 1/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1007ms, status: 1/3 UP.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1016ms, status: 0/2 DOWN.
Health check for server b_mysql_slave/slave failed, reason: External check error, code: 1, check duration: 1016ms, status: 0/2 DOWN.
Server b_mysql_slave/slave is DOWN. 0 active and 1 backup servers left. Running on backup. 0 sessions active, 0 requeued, 0 remaining in queue.
Server b_mysql_slave/slave is DOWN. 0 active and 1 backup servers left. Running on backup. 0 sessions active, 0 requeued, 0 remaining in queue.

まとめ

HAProxy の external-check で、Aurora の Read Replica が Master に昇格したときに参照させないようにすることができました。
また、Read Replica が全台ダウンしたときだけ Master を参照できることも確認できました。
独自のヘルスチェックが行えるので、他にも応用ができそうですね。

Amazon Linux で certbot(letsencrypt) を使って SSL 証明書を取得する

SSL Nginx certbot letsencrypt

※2016年5月16日現在の情報です。
バージョンが変わると動作しなくなる可能性があります。

letsencrypt のクライアントが certbot に改名したようです
(See: https://github.com/certbot/certbot)。
Amazon Linux 上で certbot-auto(letsencrypt-auto) を使って SSL 証明書を取得する手順を検証しました。

検証した環境は以下のとおりです。

  • certbot-auto(letsencrypt-auto) 0.6.0
  • Amazon Linux 2016.03
    • amzn-ami-hvm-2016.03.1.x86_64-gp2 (ami-29160d47)
  • Nginx 1.8.1

コマンドはすべて ec2-user で実行しました。

試行錯誤した結果、

https://github.com/certbot/certbot/issues/2872#issuecomment-211959637

のように pip と virtualenv のバージョンを上げたらうまくいきました。

pip と virtualenv のバージョンを上げる

sudo easy_install pip
sudo pip install --upgrade pip

pip をアップグレードすると、/usr/local/bin/pip に最新版の pip がインストールされたので、
以降はこちらを使います。

sudo /usr/local/bin/pip install --upgrade virtualenv

letsencrypt 環境を作成

virtualenv で letsencrypt 環境を作成します。

mkdir -p ~/.local/share
virtualenv -p /usr/bin/python27 ~/.local/share/letsencrypt
. ~/.local/share/letsencrypt/bin/activate

certbot-auto を実行

例では Nginx を使っていますが、Apache や certbot の Standalone Web サーバを使いたい場合は適宜変更してください。
Nginx や Apache を使う場合は予め起動しておいてください。

curl -s -L -O https://dl.eff.org/certbot-auto
chmod +x ./certbot-auto
./certbot-auto certonly --webroot -w /usr/share/nginx/html -d your.domain --agree-tos -m your@email.address --debug

以下の様な出力になったら成功です。

yum install の出力
...

Checking for new version...
Creating virtual environment...
Installing Python packages...
Installation succeeded.
Requesting root privileges to run certbot...
   sudo CERTBOT_AUTO=./certbot-auto /home/ec2-user/.local/share/letsencrypt/bin/letsencrypt certonly --webroot -w /usr/share/nginx/html -d your.domain --agree-tos -m your@email.address --debug
Version: 1.1-20080819
Version: 1.1-20080819

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/your.domain/fullchain.pem. Your
   cert will expire on 2016-08-14. To obtain a new version of the
   certificate in the future, simply run Certbot again.
 - If you lose your account credentials, you can recover through
   e-mails sent to your@email.address.
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

証明書の更新

以下のコマンドでできるようなので定期的に実行して、Web サーバを reload するようにしておくと自動更新できると思います。

./certbot-auto renew --webroot -w /usr/share/nginx/html --force-renew --debug
Checking for new version...
Requesting root privileges to run certbot...
   sudo CERTBOT_AUTO=./certbot-auto /home/ec2-user/.local/share/letsencrypt/bin/letsencrypt renew --webroot -w /usr/share/nginx/html --force-renew --debug

-------------------------------------------------------------------------------
Processing /etc/letsencrypt/renewal/your.domain.conf
-------------------------------------------------------------------------------

-------------------------------------------------------------------------------
new certificate deployed without reload, fullchain is
/etc/letsencrypt/live/your.domain/fullchain.pem
-------------------------------------------------------------------------------

Congratulations, all renewals succeeded. The following certs have been renewed:
  /etc/letsencrypt/live/your.domain/fullchain.pem (success)

Github Webhook を受けて任意のスクリプトを実行するツール

go golang github

ghooks-cmd-runner という、
Github の Webhook を受けて任意のスクリプトを実行する Go 製のツールを書きました。

Installation

https://github.com/tkuchiki/ghooks-cmd-runner/releases にバイナリがあるので、ダウンロードして解答してください。

Usage

$ ./ghooks-cmd-runner --help
usage: ghooks-cmd-runner --config=CONFIG [<flags>]

Receives Github webhooks and runs commands

Flags:
      --help              Show context-sensitive help (also try --help-long and --help-man).
  -c, --config=CONFIG     config file location
  -p, --port=18889        listen port
      --host="127.0.0.1"  listen host
  -l, --logfile=LOGFILE   log file location
      --pidfile=PIDFILE   pid file location
      --version           Show application version.

config ファイルが必要なので、以下の様な TOML ファイルを用意します。

# port = 18889 (default: 18889)
# host = "0.0.0.0 (default: 127.0.0.1)"
# secret = "your webhook secret"
# logfile = "path to logfile (default: stdout)"
# pidfile = "path to pidfile"

[[hook]]
event = "push"
command = "/path/to/script"

[[hook]]
event = "pull_request"
command = "/path/to/script"

[[hook]]eventcommand を必要な分記述して、スクリプトを用意すれば完了です。

ghooks-cmd-runnerスクリプトを実行する際、GITHUB_WEBHOOK_PAYLOAD という環境変数に payload の JSON テキストを base64エンコードした文字列を格納しますので、環境変数を読んで base64 でデコードすればお好きな言語で payload を受け取とることができます。
シェルスクリプトで書くと以下のようになると思います。

echo ${GITHUB_WEBHOOK_PAYLOAD} | base64 -d
...

daemonize の機能はないので各自で行ってください。

追記

〜 2016-05-18 17:00

以下のバージョンをリリースしています。

  • v0.1.2
    • コマンドの標準(エラー)出力を1行ずつログに書き出すように修正
  • v0.1.3
    • hook[].branch オプション追加
      • イベント毎に、正規表現にマッチした branch のみでコマンドを実行可能に
  • v0.2.0
    • イベント毎に、コマンドを直列実行するように修正

時刻文字列をいい感じに parse する Go の package

go golang

parsetime という時刻文字列をいい感じに parse する Go のライブラリを書きました。

Example

使用例です。

package main

import (
    "fmt"
    "github.com/tkuchiki/parsetime"
    "log"
)

func main() {
    p, err := parsetime.NewParseTime()
    if err != nil {
        log.Fatal(err)
    }

    t, err2 := p.Parse("2016-01-02T03:04:05")

    if err2 != nil {
        log.Fatal(err)
    }


    // Local timezone: JST
    // 2016-01-02 03:04:05 +0900 JST
    fmt.Println(t)
}

以下、NewParseTimeParse の説明です。

NewParseTime

NewParseTime は 0 ~ 2 の引数を指定可能です。
引数を指定しなければ local の *time.Location をセットした状態の time.Time を返します。
第一引数には *time.LocationAsia/TokyoJST のような文字列を指定できます。
Asia/Tokyo のような文字列を指定した場合は、
time.LoadLocation して *time.Location をセットします。
JST のような文字列を指定した場合は、
その文字列から offset を取得して time.FixedZone を実行します。
第一、二引数に JST のような文字列、offset を指定した場合も time.FixedZone を実行します。

ParseTime 構造体には、SetLocation というメソッドを用意しているので、あとから変更することも可能です。

Parse

Parse に時刻文字列を指定すると、time.Timeerror を返します。
Parse は、time.Parse ではなく、正規表現でキャプチャして time.Date を使う実装になっています。
最初に実装した時は、time.Parse の format を大量に用意しておいて、
parse できるまでループで回していたのですが、正規表現を使ったほうが柔軟に対応できそうだったので実装しなおしました。
15:04 のように、時、分だけ指定した場合、実行した年月日を設定した time.Time を返すようになっています
(time.Parse をループで回していたときはこれができていませんでした)。
対応している文字列は、ISO8601、RFC 3339、RFC822, RFC850, RFC1123 などです。
01/02/2006 15:04:0511:04 PMJan 2, 2006 at 3:04am (MST) のような形式にも対応しています。

parse 可能な時刻文字列の例は https://github.com/tkuchiki/parsetime#examples を参考にしてください。

php-build が libphp[57].so を上書きしないようになりました

php-build PHP

php-build が libphp5.so を上書きしないようにするパッチ で、
php-build が libphp5.so を上書きしてしまう問題を紹介しました。

2015-12-11 に Remove hard-patch code for APXS handling #353 が merge され、
libphp5.so(PHP 7 は libphp7.so なので、以降 libphp?.so) が上書きされないようになりました。

@tkak さんに Avoid overwriting apache module installed into user's libexec dir #204 を送って頂いていたのですが、方針と合わなかったのかこれ自体は Merge されませんでした。
送っていただいた Pull Request 自体が Merge されなかったのは残念ですが、
@tkak さんのお力添えで問題が解決されるまでに至りました。
ありがとうございました!

使い方

with_apxs2 という関数が追加されましたので、definitions に追記します。

# https://github.com/php-build/php-build/blob/master/bin/php-build#L491-L501
function with_apxs2() {
    local apxs="$1"
    shift

    if [ -z "$apxs" ]; then
        apxs="$PHP_BUILD_APXS"
    fi
    PHP_BUILD_APXS="$apxs"

    configure_option "--with-apxs2" "$apxs"
}
# https://github.com/php-build/php-build/blob/master/bin/php-build#L576-L580
# Use php-build prefix for the Apache libexec folder
if [ -n "$PHP_BUILD_APXS" ]; then
    apxs_libexecdir=$($PHP_BUILD_APXS -q LIBEXECDIR)
    sed -i"" -e "s|'\$(INSTALL_ROOT)${apxs_libexecdir}'|${PREFIX}${apxs_libexecdir}|g" ${source_path}/Makefile
fi

となっていますので、apxs に path が通っていれば with_apxs2
通っていなければ with_apxs2 /path/to/apxs と書けば libphp?.so の install path が変わります。
php-build には default_configure_options という、
全バージョン共通の configure のオプションを記述するファイルがあります。
こちらに --with-apxs2 /path/to/apxs を書いても、
libphp?.so の install path が変更されませんのでご注意ください。
また、definitions に configure_option "--with-apxs2" "/path/to/apxs" と書いても同様です。
しかし、異なるバージョンの php を install するごとに definitions を編集するのは大変です。
全部に追記したい場合は、以下のコマンドを実行すれば追記されます。

$ find /path/to/definitions -type f -print | xargs -I{} sh -c "grep -qs with_apxs2 {} || perl -p -i -e 'print \"with_apxs2 /usr/bin/apxs\n\" if $. == 1' {}"

追記済みの場合は何もしないので安心してお使いいただけるはずです。
上記のコマンドは色々な環境でも動くようにしたものですので、
GNU sed のある環境でしか使わない場合は以下のコマンドでも同じことができます。

$ find /path/to/definitions -type f -print | xargs -I{} sh -c "grep -qs with_apxs2 {} || sed -i '1i\with_apxs2 \"/usr/bin/apxs\"' {}"

まとめ

php-build が libphp?.so を上書きしてしまう問題への対応方法を紹介しました。
default_configure_options に書いたら libphp?.so の install path が変わったら楽そうだなと思いましたが、default_configure_options は全バージョン共通で使える configure option しか書かないと考えると、そういう方針なのだろうということで納得できました
(--with-apxs2 オプションは、現状 php-build が扱っている全バージョンで使えるようですが......)。
patch を当てる方法だとアップデートへの追従が大変なので、公式がサポートしてくれて良かったです。

jq で JSON を LTSV に変換する

jq json ltsv

結論

[
  {"foo": "bar1", "hoge": "piyo1"},
  {"foo": "bar2", "hoge": "piyo2"},
  {"foo": "bar3", "hoge": "piyo3"}
]

という json(test.json とする) があるときに、

$ cat test.json | jq -r '.[] | to_entries | map("\(.key):\(.value)") | join("\t")'

と実行すると、

foo:bar1        hoge:piyo1
foo:bar2        hoge:piyo2
foo:bar3        hoge:piyo3

と出力されます。

解説

分解して説明します。
to_entrieskey, value という key を持った hash の array に変換します。

$ cat test.json | jq -r '.[] | to_entries'
[
  {
    "key": "foo",
    "value": "bar1"
  },
  {
    "key": "hoge",
    "value": "piyo1"
  }
]
[
  {
    "key": "foo",
    "value": "bar2"
  },
  {
    "key": "hoge",
    "value": "piyo2"
  }
]
[
  {
    "key": "foo",
    "value": "bar3"
  },
  {
    "key": "hoge",
    "value": "piyo3"
  }
]

map は配列に対して処理を行え、
"(\.KEY)" で文字列変数展開ができるので、
map("\(.key):\(.value)") として Label:Value の形式に変換します。

$ cat test.json | jq -r '.[] | to_entries | map("\(.key):\(.value)")'
[
  "foo:bar1",
  "hoge:piyo1"
]
[
  "foo:bar2",
  "hoge:piyo2"
]
[
  "foo:bar3",
  "hoge:piyo3"
]

最後に、join("\t") で array を Tab 区切りの文字列に変換して完了です。

謝辞

http://stackoverflow.com/a/25378171 をちょっといじっただけです。
とても助かりました。aioobe さん、ありがとうございます!