ディレクトリを遡ってMakefileを探してそのディレクトリでmakeを実行するmkを作った

Goを書くようになってからなのか、GMOペパボに入ってからなのか、make を実行することが多くなったのですが、makeMakefileがあるディレクトリで実行する必要があります(もしくは -C オプションでMakefileのあるディレクトリを指定)。

ただ、rakenpm に慣れてしまっているからか、ディレクトリ関係なく make を実行してしまい *1、そのたびに make: *** No targets specified and no makefile found. Stop. を突きつけられてしまっていました。

というわけで、ディレクトリを遡ってMakefileを探してそのディレクトリで make を実行するだけのコマンド mk を作りました。

github.com

インストール

go get か Homebrew でインストールできます。

$ go get github.com/k1LoW/mk

or

$ brew install k1LoW/tap/mk

使い方

make の代わりに mk を使うだけです。

$ cat Makefile
hello:
        @echo "Hello make"
$ cd path/to/deep/
$ cat Makeflie
cat: Makefile: No such file or directory

上記のようにMakefileのあるディレクトリでないところで、make を実行してもエラーになりますが

$ make hello
make: *** No targets specified and no makefile found.  Stop.

mk だとディレクトリを遡ってMakefileを探して make を実行できます。

$ mk hello
(in /path/to/deep)
Hello make

alias make='mk' として使ってみようと思います。

*1:rakeやnpmは Rakefileやpackage.jsonディレクトリを遡って探してくれます

evryの実装をシンプルにした

evry を公開してからオンラインオフライン問わず様々な場でフィードバックをいただいて、それをもとに修正したv0.2.0をリリースしました。

github.com

指定コマンドを実行するための実装をシンプルにした

Use `sh -c` instead of go-shellwords by k1LoW · Pull Request #3 · k1LoW/evry · GitHub

もともとevryでは、オプションに渡された指定コマンドの文字列を、go-shellwords を利用してパースし、パイプ処理を go-pipelineの実装を参考に 組み込んで実行するようにしていましたが、シンプルに sh -c に文字列を渡して実行するように変更しました。

evryにオプションだけでなく引数でもコマンドを渡せるようにした

Allow specifying of command by args by k1LoW · Pull Request #4 · k1LoW/evry · GitHub

具体的には従来の -c オプションによるコマンド指定だけでなく、

$ tail -F access.log | evry -s 10 -c 'wc -l'

以下のようにそのまま引数で渡せるようにもしました。

$ tail -F access.log | evry -s 10 -- wc -l

両方利用できます。

watchコマンドと同じ引数に揃えるのはあきらめました

watch コマンドは -n (--interval) で実行間隔(秒)を指定できるのですが、evryは秒でまとめるだけでなく行数でもまとめることができるので、 --interval は使いづらく、変更せず従来のままのオプションとしました。

公開することで得られるフィードバック

ツールやコードを公開することで得られるフィードバックは本当にありがたいですし、特に今回は勉強になりました。

こちらが勝手にフィードバックを拾っていった方もいるのであえてお名前は挙げませんが、 「俺かな?」と思われた皆さんありがとうございますー!!

tail -Fなどの標準出力を一定時間ごとに指定のコマンドに渡し、その実行結果をシーケンシャルに出力するコマンド evry を作った

ログの流量やサマリを、カジュアルに、そしてリアルタイムに確認したかったので作ってみました。

github.com

これはなに

標準入力の内容を、一定時間ごと(もしくは一定行ごと)にまとめて指定のコマンドに渡し、その実行結果をシーケンシャルに出力するコマンドです。

使い方

例えば、「NGINXの10秒あたりのリクエスト数を確認する」には、以下のように evry で10秒ごとにまとめた標準入力を wc -l に渡します。

$ tail -F /var/log/nginx/access.log | evry -s 10 -c 'wc -l'
    8700
    1260
    8580
    6430
    [...]

例えば、「Apacheの1000リクエストあたりのアクセス先ランキング上位5を確認する」には、以下のように書けます(GNU sedを使用)。

$ tail -F /var/log/httpd/access.log | evry -l 1000 -c 'cut -d" " -f7 | sort | uniq -c | sort -nr | head -5 | sed "0,/^/ s/^/---\\n/g"'
---
  90 /category/electronics
  65 /category/software
  55 /category/games
  45 /category/books
  27 /category/computers
---
 108 /category/electronics
  76 /category/software
  63 /category/games
  44 /category/books
  31 /category/computers
---
  97 /category/electronics
  55 /category/games
  52 /category/software
  48 /category/books
  35 /category/office
---
  99 /category/electronics
  55 /category/software
  53 /category/games
  [...]

他のコマンドを使ったり組み合わせた書き方はあるかと思うのですが、直感的に書ける のが良いと思っています。

インストール

go get か Homebrewでインストールできます。

$ go get github.com/k1LoW/evry
$ brew install k1LoW/tap/evry

シーケンシャルに入力を受け取り、パラレルに実行し、シーケンシャルに出力する

evry を実装するにあたって実現したかったのが、

  1. 標準入力はリアルタイムに受け取って処理したい
  2. 出力は1の標準入力から受け取った順に出力したい(そのほうが 直感的 だと考えているから)

ということです。そのために、

  • 標準入力からの値を、都度 -c のコマンドに渡して、コマンド自体はそれぞれ並行に実行することによって、時間のかかるコマンドでもできるかぎり処理をブロックしないようにする
  • 並列に実行しているコマンドの実行時間によって出力順番が変わらないようにする

という挙動を実装しました。

具体的には図のように

f:id:k1LoW:20190114200758p:plain
evryの処理の流れ

  1. 標準入力から流れてくる値を一定時間もしくは一定行にまとめる(緑)
  2. 「入力の順番」を記憶する(オレンジ左)。
  3. 指定コマンドの標準入力に1でまとめた値を渡して、それぞれ並行に実行させる(青)。
  4. 実行結果の出力は記憶している「入力の順番」に出力する(オレンジ右)

という動きをします。

上記の evry の動きの例としては、以下のコマンドを実行するとわかりやすいかと思います。

$ echo "3\n5\n2" | evry -l 1 -c 'xargs -I@ sh -c "sleep @; echo @"'

コマンドは、352 をそれぞれ xargs -I@ sh -c "sleep @; echo @" に渡してその実行結果を出力する(@秒sleepしてecho @するだけ)というものです。

指定コマンドに sleep が入っているので、それぞれ3秒、5秒、2秒待つような挙動をします。

上記を実行すると、約3秒後に3が、その約2秒に 52 が一気に出力されます。

time を挟むとわかりやすいです。

$ time echo "3\n5\n2" | evry -l 1 -c 'xargs -I@ sh -c "sleep @; echo @"'
3
5
2
echo "3\n5\n2"  0.00s user 0.00s system 38% cpu 0.001 total
evry -l 1 -c 'xargs -I@ sh -c "sleep @; echo @"'  0.01s user 0.02s system 0% cpu 5.019 total

実行時間が(10秒ではなく)約5秒になっている ことがわかります。

実行時間からわかるように、'xargs -I@ sh -c "sleep @; echo @"' を並列に実行しています。

かつ、その出力順は、それぞれの実行時間(3秒、5秒、2秒)に関わらず352と元の標準入力の順を保っています。

Goの並行処理は自分にはまだ難しいけれどうまくハマると面白い

今回、evry で実現したかった「シーケンシャル->パラレル->シーケンシャル」を自分なりに sync.Mutexsync.WaitGroup とチャンネルを組み合わせて実装できました。面白かったです。

うんうん唸って設計して実装しただけに、意図したとおりに動いたときはテンションが上りました。Goで並行処理を実装する良いお題だったのかもしれません。

そして、もう少し並行処理を実装する感覚に慣れていく必要がありそうです。

今は、答え合わせの意味も含めて「Go言語による並行処理」を読み進めています。

読んでいて、全く思いついたことのないパターンに「なるほど」と思ったり、実装したことのあるパターンがすっきりと整理されていたり、とても勉強になります。「Go言語による並行処理」で学んだ内容をもとに evry の実装をもっと良くできそうな気がします。

Go言語による並行処理

Go言語による並行処理

2018年の振り返りと2019年の抱負

明けてました

2018年の振り返り

2018年は個人的にいろいろ環境が変わったので2017年と同じようにはいきませんでした。いい意味でも悪い意味でも。

2018年の目標は「努力を積み重ねる」でした。

これは、転職をして新たなチームにJOINしたことで半強制的に努力をし続けないといけない状況になりました(狙い通り)。

チームメンバー全員が全員専門領域をもって強いので(中には全方位型もいたり)、迷惑をかけないようにしつつ価値も出していくのが本当に厳しい。

特に「サーバに何かあったときの瞬発力」と「コンテナ技術とはじめとするLinuxを構成する要素の知識とアプリの実装が深く絡むときの判断力」が厳しいと感じました。これは今後の課題です。

チームメンバーだけでなく、席的に後ろにいるインフラチームや、眼の前のプロダクトチーム、CTL、技術部、Slackの先にいる東京の人たちをみていて、その動き(実装力、特定力、判断力)の速さに圧倒される状況がもよくありました。

その分、自分の技術スタックを広げることに集中できた気がします。

目標「努力を積み重ねる」に対する結果としては良かったです。この初心を忘れないでおきたいです。

2018年の個人的ハイライトとしては

  • 転職そしてGMOペパボに入社した
  • 子供が生まれた
    • 働き方が大きく変わることになりました。チームメンバーにも会社にもいろいろ配慮してもらっていて感謝しかありません。まだ試行錯誤段階ですが、一段落したらまとめたいと思っています。
  • プリンシパルエンジニアになった

の3つです。

特に子供が生まれたことで他のハイライトがすべて吹っ飛びました。2018年買ってよかった大賞も圧倒的にピジョン 電子レンジスチームです。

OSS活動

Go製のものが増えました。あと実装力や技術領域に慣れていないことが原因かもしれませんが、作り上げるのに時間がかかることが多くなりました。ポジティブには捉えています。

ちなみに個人的お気に入りは tbls なのですが、いまいち認知度があがりません。2019年にもう1手打ちたいと思っていて、2018年に少し仕込んでいます。

発表

2017年と比べると少なくなりました。特に後半は家庭の事情で応募自体を控えていました。

2019年の抱負

今年は「変化に適応することを楽しむ」です。公私ともに環境が大きくかわったところで2018年が終わりました。2019年はその変化した環境からスタートです。

今までと同じような生活や、今までと同じようなアウトプットをしていくことはおそらくできないでしょう。

同じでないことは許容しても、そのせいで様々な面で劣化してしまうことは避けたいです。

変化した環境に適応して楽しんで、そして力にするところまでを目指したいです。

というわけで

今年もどうぞよろしくお願いします。

~/.ssh/configを使って *ssh.Client を作成する

GMOペパボ Advent Calendar 2018 の4日目の記事です。

運用しているサーバに何か問題が発生したら、SSH接続をして原因の特定をします。特定するためにいろいろ確認をします。

しかし、自分はチームメンバーの中では上記があまり速いほうではありません。勘所もまだまだ悪いです。

そこで、その差を埋めるべく、最低限の確認を一気にできるようにしようと考えてGoでツールを作ろうとしています(おそらくシェルスクリプトでもAnsibleでもいいのですが、なんとなく作った方が良さそうな気がしています。まだなんとなくで確証はありません)。

~/.ssh/config を読む

そこで、まずはSSHクライアントを書こうと思ったのですが、GoにはRubyNet::SSH::Config のような ~/.ssh/config を読むような機能は標準パッケージにはないようです。

探してみたところ、kevinburke/ssh_config に ~/.ssh/config を読む機能があったので、これを使って ~/.ssh/config の設定情報を使ってSSH接続をしてみます。

まず以下のような ~/.ssh/config があるとして

Host myhost
  HostName 203.0.113.1
  User k1low
  Port 10022
  IdentityFile /path/to/myhost_rsa

以下のように kevinburke/ssh_config で ~/.ssh/config を読んで、それを ssh.ClientConfig に設定することで、~/.ssh/configの情報でSSH接続ができます。

package main

import (
  "bytes"
  "io/ioutil"
  "log"

  "github.com/kevinburke/ssh_config"
  "golang.org/x/crypto/ssh"
)

func main() {
  host := "myhost"
  user := ssh_config.Get(host, "User")
  addr := ssh_config.Get(host, "Hostname") + ":" + ssh_config.Get(host, "Port")
  auth := []ssh.AuthMethod{}
  key, _ := ioutil.ReadFile(ssh_config.Get(host, "IdentityFile"))
  signer, _ := ssh.ParsePrivateKey(key)
  auth = append(auth, ssh.PublicKeys(signer))
  sshConfig := &ssh.ClientConfig{
    User:            user,
    Auth:            auth,
    HostKeyCallback: ssh.InsecureIgnoreHostKey(), // FIXME
  }
  client, _ := ssh.Dial("tcp", addr, sshConfig)
  session, _ := client.NewSession()
  defer session.Close()
  var stdout = &bytes.Buffer{}
  session.Stdout = stdout
  err = session.Run("hostname")
  if err != nil {
    log.Fatalf("error: %v", err)
  }
  log.Printf("result: %s", stdout.String())
}

(エラー処理などいろいろ省略)

ProxyCommandに対応してみる

対象サーバの前段に踏み台サーバがある場合、ProxyCommand を記述して利用します。

kevinburke/ssh_configssh_config をパースするライブラリで、ProxyCommand を解釈して実行するところまではサポートしません。

ところで、ProxyCommandに記載されているコマンドはローカルで実行されます。そして、大抵はProxyCommand に書かれた ssh -Wnc を使って確立したSSH通信を経由して(プロキシして)、目的のホストへSSH接続をすることになります。

ようは、通信をパイプでつなげられればいいはずなので net.Pipe() を使います。

c, s := net.Pipe()
cmd := exec.Command("sh", "-c", proxyCommand)
cmd.Stdin = s
cmd.Stdout = s
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
  return nil, err
}
conn, incomingChannels, incomingRequests, err := ssh.NewClientConn(c, addr, sshConfig)
if err != nil {
  return nil, err
}
client := ssh.NewClient(conn, incomingChannels, incomingRequests)

%h の変換処理などいろいろ省略)

これでProxyCommandを利用したSSH接続ができます。

sshc

上記に書かれているような処理をまとめてsshcというパッケージを作りはじめました。

github.com

使い方は以下のような感じで sshc.NewClient("myhost") で ~/.ssh/config を解釈してProxyCommandでのプロキシもした *ssh.Client を得ることができます。

package main

import (
    "bytes"
    "log"

    "github.com/k1LoW/sshc"
)

func main() {
    client, _ := sshc.NewClient("myhost")
    session, _ := client.NewSession()
    defer session.Close()
    var stdout = &bytes.Buffer{}
    session.Stdout = stdout
    err = session.Run("hostname")
    if err != nil {
        log.Fatalf("error: %v", err)
    }
    log.Printf("result: %s", stdout.String())
}

とりあえず手元にある ~/.ssh/config に対応できればいいかな、というゆるい感じで作っています。

TODOとしては

  • 公開鍵のパスフレーズに対応
  • 現在書いているテストが実際にSSH接続が確立するかのテストで公開できないのでなんとかしたい
    • 多段SSHのテストってどう書けばいいのですかね。。
  • exec.Command() を使っていて結局 ssh コマンドを使っている
    • 若干あきらめています
  • kevinburke/ssh_configssh_config のファイルパスがprivateになっていて変更できない
    • Pull Request案件

など。

動くようになったのでまずは作りたかったツール作成に移りたいと思っています。

auditdのログをlogrotateでローテートする運用にしたかったがうまくいかなかったのでcronで直接ローテートするようにした

紆余曲折の記録です。

Auditとは何か

Linux AuditはLinuxのシステムで発生しているイベントを記録するための仕組みです。詳しくは以下のページ

第5章 システム監査 - Red Hat Customer Portal

具体的には、(デフォルトで) /var/log/audit/audit.logシステムコールやユーザが実行したコマンドなどの様々なシステムイベントを記録するサービス auditd が起動することになります。

auditdのログ (audit.log) を長期保存したい

audit.logはそのまま監査ログとして利用できるため、一定期間保存しようと考えます。

しかし、前述のようにauditdは様々なシステムイベントを記録するために単純にログの保存を続けるとかなりのディスク容量を消費していきます。

そのためログローテーションの設定を組むのが一般的であり、それ以前にauditd自体にはログローテーションの機構が備わっており、デフォルトで有効になっています。

しかし、そのままローテートをしていると端から消えていくことになるので、ローテートされた古いログを保存する仕組みが必要になります。

あわせて、以下の3つの要件を実現しようと考えました。

  1. 時間ベースでローテートをしたい
    • あとでログを見つけやすくするため。ログローテートされたaudit.logに時刻のプレフィックス (ex. audit.log-2018111115 ) をつけたい
  2. ローテーション時に圧縮して保存したい
    • ログファイルが大きくなりがちのため
  3. audit.logのみで保存したい

ぱっと見、logrotateに任せたほうが良さそうです(しかし、後述しますが失敗しました)

logrotateの前に検討したいくつかの方法

auditd の設定でなんとかする

auditd.confの設定パラメータを見る限りでは時間ベースのローテートの仕組みや古いログを圧縮するためのフック機構などはなさそうでした。

audisp pluginを利用してsyslogに記録する

audisp pluginを利用して、auditdのログをsyslogに記録して、syslogに設定したログローテーションにのっかるという方法があります。

  • auditd.conf で dispatcher = /sbin/audispd を指定
  • /etc/audisp/plugins.d/syslog.conf を有効化
active = yes
direction = out
path = builtin_syslog
type = builtin
args = LOG_INFO
format = string

これでsyslogがlogrotateでログローテートされてれば、合わせてaudit.logの情報もローテートされるという仕掛けです。

しかし、今回はaudit.logのみで保存をしたいのとsyslogにもよく参照する情報があって混在するのを避けたかったため、この方法は見送ることにしました。

audit.logを直接logrotateの仕組みに載せる -> 失敗

これが本命 ですでした。

auditdが独自にログローテーション機能を止めてlogrotateの仕組みに載せることができれば、「時間ベースのローテーション」も「古いログの圧縮保存」も「audit.logのみでのローテーション」も実現できると考えました。

まず、/etc/audit/auditd.conf の max_log_file_action の設定を変更して auditdのログローテーションを止めます。

max_log_file_action = IGNORE

そしてlogrotateの設定を追加します。

   /var/log/audit/audit.log
    {
      rotate 24
      hourly
      ifempty
      dateext
      missingok
      compress
      delaycompress
    }

これでauditdを再起動して、確認しましたが 結果、うまくいきませんでした

ログファイルはローテートするのですが、auditdが audit.log ではなく、ローテートしたほうの audit.log-2018111115 の方にそのままログを書き続けてしまいました。

原因は、auditdが最初に掴んだログファイルのファイルディスクリプタにそのまま書き込んでいるからだと予想できます。

ちなみに、logrotateで考えるべきことについては @catatsuy さんの以下のエントリがとてもわかりやすかったです。

medium.com

ここまでで、auditdは上記エントリで示されている「ちゃんとローテートをしたい場合」の3つの方法のうちの 「1. ファイルに書き込む度にファイルをopenして書き込み後にcloseする」という挙動にも「3. ファイル名が変更されたタイミングを自力で検知して、元のファイル名でopenし直す」 という挙動にもなっていないということがわかりました。

なので、最後に残った「2. ファイル名が変更されたタイミングを教えてもらい、元のファイル名でopenし直す」方法を考えてみました。

しかし、auditdにはNGINXのUSR1のような「元のファイル名でopenし直す」ようなシグナルは用意されておらず、これも断念することになりました。おそらくauditd自体がログローテーションの仕組みを持っているので、そのようなシグナルを受け取る処理を作る必要がなかったのではないか?と予想しました。

logrotateで、ファイルディスクリプタを掴むようなプロセスのときに使うらしい copytruncate オプションを付与すれば一見うまくいくのですが、ログファイルをコピーして(copy)元のファイルの内容を消去する(truncate)するときにログを一部ロストしてしまう可能性があるということで(監査ログという意味でも)選択できませんでした。

cronで直接ローテートするようなスクリプトを組む

というわけでlogrotateの仕組みに載せるのはあきらめて、直接cronをトリガーに意図したログローテーションをするようにスクリプトを組みました。

#!/bin/bash

set -eu

DATEEXT=`date +%Y%m%d%H`
KEEP=5
[ -f /var/run/auditd.pid ] && kill -USR1 `cat /var/run/auditd.pid`
[ -f /var/log/audit/audit.log.1 ] && mv /var/log/audit/audit.log.1 /var/log/audit/audit.log-${DATEEXT}
[ -f /var/log/audit/audit.log-${DATEEXT} ] && gzip -f /var/log/audit/audit.log-${DATEEXT}
ls -t /var/app/audit.log-* | tail -n+${KEEP} | xargs --no-run-if-empty -i mv {} /backup/'

仕組みとしては以下のページの方法と同じです。

How to implement audit log rotation with compression based on time instead of size - Red Hat Customer Portal

  1. まずUSR1シグナルをauditdプロセスに渡すことで、auditdのログローテーション機能を実行します。
  2. 生成された audit.log.1 を時刻つきファイル名にリネームします
  3. 2でリネームしたファイルをgzipで圧縮します
  4. 古いログファイルから移動します

上記スクリプトをcronで1時間ごとに実行することで、なんとかやりたいことを実現することができました。

結局

audit.logを時間でローテートするベストな方法は何だったのか。。是非どなたか教えて欲しいです。

ここまでくるのに時間がかかってしまった。。

Fukuoka.go#12 で GoでTCPパケットを読むための最初の一歩の話 をした #fukuokago

Fukuoka.go に参加してきました!

fukuokago.connpass.com

主催者、発表者のみなさん、ありがとうございました!

ちょっとトラブルがあって楽しみにしていた「Road to your goroutines」をまともに聞けなかったので残念がっていたら、素晴らしいまとめエントリが上がっていて本当に感謝です!!

GoでTCPパケットを読む

最近、tcpdpというTCPパケットを読むツールを作っているのですが、その開発をするにあたって「Goもやっぱりライブラリが揃っているな」と感じたので、そういった意味での発表をしました。

tcpdpの話をすると、プロトコルの解析苦労話になりそうだったのでやめときました(Goっぽくない)

google/gopacket

スライドを見てもらったらわかるとおり、ほとんど google/gopacket の紹介みたいになっています。

それでも、あまりにも簡単にtcpdumpぽいものが作れてしまうので、最初、個人的には本当にびっくりしました。

発表で作った簡易tcpdumpはわずか32行です。

package main

import (
  "encoding/hex"
  "fmt"
  "log"

  "github.com/google/gopacket"
  "github.com/google/gopacket/pcap"
)

func main() {
  device := "en0"
  filter := "tcp and port 80"

  handle, err := pcap.OpenLive(
    device, int32(0xFFFF), true, pcap.BlockForever,
  )
  if err != nil {
    log.Fatal(err)
  }
  defer handle.Close()
  if err := handle.SetBPFFilter(filter); err != nil {
    log.Fatal(err)
  }

  packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
  for packet := range packetSource.Packets() {
    fmt.Printf("%s\n", packet)
    fmt.Printf("%s", hex.Dump(packet.Data()))
  }
}

たったこれだけで動くtcpdumpっぽいコマンドが出来上がりです。

そうするとあとは、拡張したいと思ったところをいじればいいし、気になるところはコードから定義ジャンプをしていけば理解が深まります。

書いて動くと理解が進む

発表資料をみたらわかるように、自分はネットワーク周辺は雰囲気で触っていることが多いです(注意: 上記ツイートはわざと面白おかしくしてもらった結果です)

しかし、実際にコードに落とし込んで動くようになると、次の試行錯誤をすることができます。

試行錯誤ができるところまでできたら、次の「使えるツールが作れるかも」というモチベーションを持った開発ができます。

モチベーションを持てるとちょっとばかり面倒な部分についても調べたり検証したりする気になります。

そして、結果として周辺の理解が進むと感じています。

というわけで

今回の発表で、自分と同じように「ネットワーク周り得意じゃないな」と思っている人がちょっとでも「Goで書けるならやってみようかな」と思ってくれたら幸いです。

そういえば、今週末もずっと []byte{} を眺めてたな。。。