nabeo がピーしているブログ (仮)

どーも、nabeop です

手元で Dockerfile から docker イメージを作る時に使っている Makefile の書き方

手元環境で Dockerfile から docker イメージを作る時はいちいち docker build コマンドを直接実行するのはダルいのでラッパースクリプトなどで実行するようにしています。

管理している Dockerfile や docker イメージの種類が多くなってくると素朴なラッパースクリプトでは作成したい docker イメージを狙い撃ちで生成するにはラッパースクリプトで一工夫が必要になります。これだとラッパースクリプトが必要以上に多機能になってしまい、作業の本質以外に気を取られてしまいます。

古くから使われている GNU make を使えば、Dockefile から docker イメージを作成するルールをいい感じに作れたので、作り方のメモを書いておきます。

まず、Makefile のルールの書き方は

生成物(ターゲット名): 生成元のリスト

といった感じに書いていきます。ターゲット名で指定しているファイルと生成元のリストで列挙されているファイルのタイムスタンプを比較して、生成物よりも新しい生成元のファイルがあればルールが実行されます。

ただし、docker build で生成されるのは docker イメージなので、ファイルのタイムスタンプを取ることが GNU make の世界ではできません。そこで docker build の後に docker inspect コンテナイメージ | jq -r .[].RepoTags[] > .build といったように生成したコンテナイメージの情報を書いたファイルを GNU make の世界での生成物とするうにします。以下のような感じです。

.build: Dockerfile
  docker build -t hoge:latest -f Dockerfile .
  docker inspect hoge:latest | jq -r .[].RepoTags[] > .build

生成物である .build ファイルに記述する内容は生成した docker イメージが特定できればなんでも良いと思いますが、 make clean などで消す時の利便性を考えて docker イメージのレポジトリとタグ名を入れています。clean ターゲットは以下のように書いています

clean:
  cat .build | xargs -I {} docker rmi {}

また、docker で multi stage build を使用している場合はターゲットの生成元にベースになる Docker イメージを生成した時の .build ファイルを指定しておけば依存関係に組み込まれて便利です。

.build-base: Dockerfile.base
  docker build -t base:latest -f Dockerfile.base .
  docker inspect base:latest | jq -r .[].RepoTags[] > .build-base

.build: Dockerfile .build-base
  docker build -t hoge:latest -f Dockerfile .
  docker inspect hoge:latest | jq -r .[].RepoTags[] > .build

さらに Dockerfile 内で ADD 命令や COPY 命令などでコンテナイメージにファイルを配置しておきたい場合は Dockefile を置いているディレクトリに roo-hogehoge ディレクトリをコンテナ内の / として見立てて配置しておくと Makefile のターゲットでは $(shell find root-hogehoge -type f) として依存関係に組み込んでいます。

具体的には nginx を nginx-build で作るときに -c で指定するスクリプトをコンテナ内の /tmp/nginx-build/nginx_configure.s として使用するときは以下のように配置しています。

.
├── Dockerfile
├── Makefile
└── root
    └── tmp
        └── nginx-build
            └── nginx_configure.sh

このときの Makefile は以下のようにターゲットを指定しています。

.build: Dockerfile $(shell find root -type f)
  docker build -t nginx:latest -f Dockerfile .
  docker inspect nginx:latest | jq -r .[].RepoTags[] > .build

実際に使っている Makefile は以下のようになっています

MAKEFLAGS = -j 4
DOCKER_BUILD_ARGS := --no-cache --squash --rm
IMAGE_NAME := varnish
USER_NAME := $(shell whoami)
BUILD_FILES = .build-4.1 .build-5.2 .build-6.2 .build-6.0lts

.PHONY: build
build: $(BUILD_FILES)

.build-4.1: Dockerfile.jessie
  docker build $(DOCKER_BUILD_ARGS) --build-arg VARNISH_VERSION=41 -t '$(USER_NAME)/$(IMAGE_NAME):jessie-4.1' -f Dockerfile.jessie .
  docker inspect '$(USER_NAME)/$(IMAGE_NAME):jessie-4.1' | jq -r '.[].RepoTags[]' > .build-4.1

.build-5.2: Dockerfile.jessie
  docker build $(DOCKER_BUILD_ARGS) --build-arg VARNISH_VERSION=52 -t '$(USER_NAME)/$(IMAGE_NAME):jessie-5.2' -f Dockerfile.jessie .
  docker inspect '$(USER_NAME)/$(IMAGE_NAME):jessie-5.2' | jq -r '.[].RepoTags[]' > .build-5.2

.build-6.2: Dockerfile.stretch
  docker build $(DOCKER_BUILD_ARGS) --build-arg VARNISH_VERSION=62 -t '$(USER_NAME)/$(IMAGE_NAME):stretch-6.2' -f Dockerfile.stretch .
  docker inspect '$(USER_NAME)/$(IMAGE_NAME):stretch-6.2' | jq -r '.[].RepoTags[]' > .build-6.2

.build-6.0lts: Dockerfile.stretch
  docker build $(DOCKER_BUILD_ARGS) --build-arg VARNISH_VERSION=60lts -t '$(USER_NAME)/$(IMAGE_NAME):stretch-6.0lts' -f Dockerfile.stretch .
  docker inspect '$(USER_NAME)/$(IMAGE_NAME):stretch-6.0lts' | jq -r '.[].RepoTags[]' > .build-6.0lts

.PHONY: clean
clean: $(BUILD_FILES)
  cat $^ | xargs -I {} docker rmi {}
  rm -f $^

BUILD_FILES に対象となる .build 相当のファイルを列挙して、build ターゲットや clean ターゲットで指定しておくことで -j オプションで並列化できるので便利です。

なんらかの CI/CD パイプラインを構築している場合はこのままでは使えないけど、手元でテストする分にはこんな感じで割と便利に使えています。

コードの書き納めと書き初めで mackerel plugin を作った

2018年から2019年の年末年始は 1/4(金) を有給にして、12/29 から 1/6 までガッツリと休みを確保したので、チマチマとコードを書いていた。

Linuxproc/net 以下に出ている情報をなんとかして、mackerel で表示できたら便利だよなーと思っていたんだけど、ちょうど時間がガッツリとれたので、年末年始の書き納めと書き初めという体で以下の3つのプラグインを作ってみた。

github.com github.com github.com

mackerel-plugin-proc-net-ip_vs_stats_percpu

2018 年のコードの書き納めとして作りました。

/proc/net/ip_vs_stats_percpu を情報源に CPU コアごとに処理したパケットの統計情報が取れる。LVS-HOWTO の 33. LVS: Monitoring では /proc/net/ip_vs_stats でいい感じな情報が取れる、ということだったけど、僕が確認した kernel 3.6 系だと real server ごとの統計情報が表示されていなかった。そのかわり /proc/net/ip_vs_stats_percpu では CPU コアごとに処理したパケット数とバイト数が取れるようだったので、こっちを使うことにした。 /proc/net/ip_vs_stats_percpulinux/ip_vs_ctl.c at master · torvalds/linux · GitHub を見る限り、意図した情報が取れそうな気配です。

mackerel-plugin-proc-net-ip_vs_stats_percpu では引数に -cpus で CPU コア数を明示できるようにしていますが、内部で runtime.NumCPU() で CPU コア数を取得するようにもしています。なんでこんなことをしているか、というと仮想化されたホストで LVS を使っていると /proc/net/ip_vs_stats_percpu で表示される CPU コアが実際のコア数よりも多くなっているケースがあるからです。runtime.NumCPU() に任せれば良えやんという話もあるかもしれませんが、なんとなくオプションで渡してあげると便利かも、と思ってつけました。

mackerel-plugin-proc-net-arp

2019 年のコードの書き初めとして作りました。

/proc/net/arp を情報源にホストの ARP テーブルのサイズが取れます。本当は Flags ごとの統計も取れたら便利かなーと思ったんですけど、とりあえずテーブルサイズだけでも取っておけば、ARP テーブル溢れとかに気付けるかと思って、最小限の機能だけにしました。

mackerel-plugin-proc-net-ip_vs

mackerel-plugin-proc-net-ip_vs_stats_percpu を作っているときに LVS-HOWTO や linux/net/netfilter/ipvs at master · torvalds/linux · GitHub を眺めていて、 ipvsadm -nL 相当の情報を /proc/net/ip_vs から取得できそうだったので作りました。

ipvsadm -nL の出力を情報源にする、というアプローチもあると思いますが、個人的には ipvsadm という cli ツールに頼らずに procfs から情報が取れるなら、procfs を利用したほうが依存関係がスッキリしていいんじゃないかと思っています。

というわけで

今年も健やかにコードが書けることを祈念して、書き初めをしてました。今年も良い年でありますように。

mackerel-agent-plugins に PR を送った話

Mackerel Advent Calendar 2018 - Qiita の 11日目です。昨日は id:koudenpa さんの.NET で動くアプリケーションを Mackerel で監視できるかな? でした。

今回は mackerel-agent-plugin の mackerel-plugin-squid を使おうとしたところ、運用上欲しいメトリックが対象となっていなかったので、エイやと機能を追加して、取り込んでもらったので、その体験談です。

メトリックとして欲しい項目を考える

今回は既存の plugin に欲しい項目を付け加えるので、コードを読んでみました。

squid には各種情報をとるためのインターフェースがいくつか用意されていて、mackerel-plugin-squid では mgr:info を情報源にしていました。どのような情報が取れるかはリンク先を参照してもらうとして、今回、僕が是非とも加えたいと思ったのは、以下の項目です。

  • squid プロセスが使用した CPU 時間
  • squid が握っている FD の情報
  • squid のキャッシュストレージの使用状況
  • squid のメモリ確保状況

それぞれ、mgr:info から取得するには以下の項目を参照すれば良さそうです。

  • squid プロセスが使用した CPU 時間
    • Resource usage for squid:CPU Usage, 5 minute avg:
  • squid が握っている FD の情報
    • File descriptor usage for squid: あたりを丸ごと
  • squid のキャッシュストレージの使用状況
    • Cache information for squid:Storage Swap size:Storage Mem size: あたりが使えそう
  • squid のメモリ確保状況
    • Memory accounted for:memPoolAlloc calls:memPoolFree calls: あたりが使えそう

修正箇所を考える

mackerel-agent-plugin の書き方は整備されたドキュメントが公開されているので、そこを参考に書き方を調べます。

今回は既存の plugin に修正するので、既存のコードを眺めます。ポイントはグラフ定義部分とメトリック値を入れているところです。

グラフ定義部分は GraphDefinition() という関数の中で graphdef を返しているだけなので、graphdef を眺めてなんとなく雰囲気を掴みます。今回は Memory accounted for:memPoolAlloc calls:memPoolFree calls: の値はカウンター値なので、そこだけ気をつけてあげれば良さそうです。

また、メトリック値を取得しているところは僕が最初に確認した時は FetchMetrics() の中で TCP ソケットを開いて GET cache_object://localhost:3128/info HTTP/1.0\n\n というような文字列を直接流し込んでいました。最近の squid だと localhost:3128 など cache_manager のポートに HTTP 通信をさせると、取得できるのですが、このプラグンが最初に作られた時は squid 2 系についても考慮する必要があったので、TCP ソケットと直接通信させる必要があったようです。また、値については応答内容を愚直に regexp.MustCompile()正規表現で拾い上げているようでした。

あと、コードを読みながら気づいたのですが、このプラグインにはテストが書かれていなかったので、テストが書きやすい構造に直してあげる必要がありそうです。

パッチを書いたら PR を出す

というわけで、諸々で書いた結果以下のような PR が出来上がりました。

https://github.com/mackerelio/mackerel-agent-plugins/pull/534

今までの PR の内容をみつつ、雰囲気を感じながら拙い英語で PR を出したところ、わりとさっくりとマージしてもらえました。

mackerel-plugin-squidgo-mackerel-plugin-helper を使っています。このライブラリはレポジトリの README.md に書かれている通り、現在は go-mackerel-plugin の使用が推奨されています。実際に最初に参照した開発向けドキュメントでも go-mackerel-plugin の使用を前提としてます。修正するときに一緒にライブラリも変更しようかと思いましたが、今回はあまり時間がとれなかったので、ライブラリの変更を見送ったのが心残りです。

mackerel-agent だけでなく、mackerel-agent-plugins も Apache License, Version 2.0 で公開されているので、自分が欲しい機能などがあれば積極的に PR を送っていきたいですね!

ubuntu で Dtrace が使える MySQL バイナリを作る

とあることで MySQL さんの働きっぷりを dtrace で追いかけようと思ったけど、ubuntu が提供しているバイナリでは、dtrace 向けのオプションが付いていない状態でビルドされていた。

$ sudo /usr/share/bcc/tools/tplist -l `which mysqld`
$

MySQL :: MySQL 5.7 Reference Manual :: 2.9.4 MySQL Source-Configuration Options を見ると cmake に -DENABLE_DTRACE=1 を渡せば良いらしい

というわけで、deb パッケージの作り方をググりながら MySQL を作り直してみた、というメモです。

まずはビルドに必要な最小限のパッケージを取ってくる

$ sudo apt install build-essential devscripts systemtap-sdt-dev
$ sudo apt build-dep mysql-server

で、ubuntu のレポジトリから MySQL パッケージを作成するために必要なファイル1式を取ってくる

$ mkdir tmp
$ cd tmp
$ apt source mysql-server

この状態で作業用ディレクトリには以下のようにファイルとディレクトリが生成されているはず。

$ ls -F
mysql-5.7-5.7.23/   mysql-5.7_5.7.23-0ubuntu0.16.04.1.debian.tar.xz  mysql-5.7_5.7.23-0ubuntu0.16.04.1.dsc  mysql-5.7_5.7.23.orig.tar.gz
$

deb パッケージ作成時にしようする cmake のオプションは mysql-5.7-5.7.23/debian/rules に書いてあるので、修正してあげる。

ここまできたら以下を実行して deb パッケージが作れるはず

$ cd mysql-5.7-5.7.23
$ debuild -uc -us -b

出来上がったバイナリを tplist -l で確認すると以下のようにそれっぽい感じになっている。わーい。

$ sudo /usr/share/bcc/tools/tplist -l mysql-5.7-5.7.23/builddir/sql/mysqld  | wc -l
56
$

unbound-anchor で DNSSEC 向けトラストアンカーを更新すると EXIT CODE 1 で終了する

趣味活動で unbound の dnstap とか EDNS Client Subnet の挙動を確認したかったので、unbound のコンテナイメージを作っていたんだけど、最後に /usr/local/etc/unbound 以下の環境を作るところで、エラー終了してホゲーとなっていた。問題になっていたのは以下の RUN 命令部分。

 RUN addgroup unbound && adduser -S -G unbound unbound && \
     /usr/local/sbin/unbound-control-setup && \
     /usr/local/sbin/unbound-anchor -a /usr/local/etc/unbound/root.key && \
     mkdir -p /usr/local/etc/unbound/dnstap && \
     chown unbound:unbound /usr/local/etc/unbound/dnstap

で、ブログの表題でほとんどネタバレしているんだけど、原因になっていたのは /usr/local/sbin/unbound-anchor -a /usr/local/etc/unbound/root.key && \ の部分。unbound を運用している人にとっては当たり前かもしれないけど、unbound-anchor でトラストアンカーを更新した時は終了コードが1になるらしい。unbound-anchor のマニュアルにも以下のようにキッチリ明示されている。

EXIT CODE This tool exits with value 1 if the root anchor was updated using the certificate or if the builtin root-anchor was used. It exits with code 0 if no update was necessary, if the update was possible with RFC5011 tracking, or if an error occurred.

   You can check the exit value in this manner:
        unbound-anchor -a "root.key" || logger "Please check root.key"
   Or something more suitable for your operational environment.

しかも、解決方法も書いてあるし...

とりあえず、以下のように手元の Dockerfile を書き直して最後まで完走できることは確認した。

RUN addgroup unbound && adduser -S -G unbound unbound && \
    unbound-control-setup && \
    /usr/local/sbin/unbound-anchor -a /usr/local/etc/unbound/root.key || echo "hoge" && \
    mkdir -p /usr/local/etc/unbound/dnstap && \
    chown unbound:unbound /usr/local/etc/unbound/dnstap

mkr の emacs-helm インターフェースを作ってみた

helm て何

世間では k8s が流行ているので、helm といったら、k8s のパッケージシステムの helm が一般的になりつつあって少し残念な気分になっています。

emacs の世界で helm というと、こちら。僕も emacs で生活しているので、しょっちゅう helm のお世話になっている。helm が使えなくなると、仕事の効率が激しく落ちてしまうレベルで依存しています。ここでは helm というと混乱しそうなので、emacs-helm と記述することにします。

mkr の出力を emacs-helm でも扱いたい

対して、mackerel.io も無くなったら途方にくれるレベルで仕事に使っているので、emacs の中から mackerel.io に登録しているホストのメトリクス情報や監視結果を参照できるようにしたいなー、と思っていた。

mackerel.io には mkr というナイスな CLI ツールがあるので、この出力を emacs-helm に食べさせてよしなに扱えればとりあえず目的は達成できそう。mkr は結果を json 形式で出力してくれる。emacsjson データの扱い方がわからなかったので、そのままにしていたんだけど、ある日、helm-aws というパッケージを見ていると、そのものズバリな感じで扱っていたので、やる気が出てきた。

作った

というわけで、エイヤーと作ってみたので、github.com に公開してみます。melpa.org には登録していないので、以下のように emacs の load-path が通ったところにレポジトリごと git clone してください。

git clone https://github.com/nabeo/helm-mkr.git path/to/emacsd/site-lisp/helm-mkr

mkr を使える環境を整えて、~/.emacs.d/init.el とかに以下のように書いておいてください。

(require 'helm-mkr)
(setq mkr-org "your mackerelio org name")

mkr-org は mackerel.io の URL にはオーガニゼーションの文字列が含まれるので、自身のアカウントが所属しているオーガニゼーション名をいれておく必要があります。

ここまで準備が整ったら、M-x helm-mkr*helm-mkr* バッファに mackerel で管理しているホストの一覧が出ててきます。

今の所、デフォルトのアクションは選択したホストのホスト名をコピーします。アクションの候補として、browse-url 経由で選択したホストで収集されたメトリクスページに移動できるアクションを登録しています。

現状ではこんな感じだけでど、アラートのリストまで取れるようになったら(あと、要望とかあれば)、mepla.org とか emacs のパッケージ配布サイトに登録して手軽に使えるようにしたい。

infrataster-plugin-dns で DNS サーバの検査をする

DNS サーバの挙動をテストする

インフラの挙動をテストするためのフレームワークである infrataster という仕組みがあります。サーバ構築時のテストフレームワークServerspec が有名ですが、対象をインフラにしたものという乱暴な理解です。

で、インフラといえば DNS なんですが、僕は小心者なので DNS サーバの設定でしくじったりすると、被害が甚大なので、小さな修正でもドキドキしながらオペレーションをしています。また、設定直後はうまく動いているように見えて、実は他のエントリを書き換えてしまい後から障害になってしまう、ということも考えられます。まさに自分が自宅の内部ネットワークのメンテ時に凡ミスして一時的に名前解決ができなくなってツライ目にあってしまったので、infrataster の復習がてら、infrataster-plugin-dns を使ってみたので、そのメモを残しておこうと思います。

infrataster そのものは DNS サーバのテストはできませんが、infrataster-plugin-dns はその名前のとおり infrataster のプラグインとして作成されて、特定の DNS サーバを対象にしてクエリ応答のテストができます。

準備

infrataster は rspec の仕組みのうえに構築されているので、rspec --initrspec 的な準備をしておきます。

spec/spec_helper.rb には以下のように検査したい DNS サーバを定義します。

require 'infrataster/rspec'
require 'infrataster-plugin-dns'

Infrataster::Server.define(:exapmle_com, 'a.iana-servers.net')

ここでは例として example.com の権威サーバの a.iana-servers.net:example_com として定義しています。

テストケースは spec/example.com_spec.rb とかに書いていきます。

require  'spec_helper'

describe server(:exapmle_com) do
  # ここにテストケースを書いていく
end

頑張ってテストケースを書く

ここからは運用している DNS サーバに登録している内容をひたすら列挙していく作業です。

infrataster-plugin-dnsREADME.md にも書かれていますが、基本的に rspec 的なマッチャーは rspec-dns をベースにしているので、そちらを参照していきます。さらに、rspec-dns は内部的に Dnsruby を呼び出しているので、各 RR の応答を検査するときは Dnsruby::RR クラスのドキュメントを読みながら空気を察する必要があります。

というわけで、いくつか例をあげておきます。

NS レコード

example.com. の NS の場合は dig で確認すると以下のように応答がありました。

;; QUESTION SECTION:
;example.com.                   IN      NS

;; ANSWER SECTION:
example.com.            86400   IN      NS      a.iana-servers.net.
example.com.            86400   IN      NS      b.iana-servers.net.

この場合は Dnsruby::RR::NS を見ながら書くとこんな感じになります。

describe dns('example.com.') do
  it 'NS RR : a.iana-servers.net' do
    is_expected.to have_entry.with_type('NS').
                     and_ttl(86400).
                     and_domainname('a.iana-servers.net')
  end
  it 'NS RR : b.iana-servers.net' do
    is_expected.to have_entry.with_type('NS').
                     and_ttl(86400).
                     and_domainname('b.iana-servers.net')
  end
end

SOA レコード

example.comSOAdig で確認すると以下のようになっています。

;; QUESTION SECTION:
;example.com.                   IN      SOA

;; ANSWER SECTION:
example.com.            3600    IN      SOA     sns.dns.icann.org. noc.dns.icann.org. 2018013047 7200 3600 1209600 3600

で、NS の時と同じように Dnsruby::RR::SOAアトリビュートとRR応答の仕様を見比べながら書いていきます。

describe dns('example.com.') do
  it 'SOA RR' do
    is_expected.to have_entry.with_type('SOA').
                     and_ttl(3600).
                     and_mname('sns.dns.icann.org').
                     and_rname('noc.dns.icann.org').
                     and_refresh(7200).
                     and_retry(3600).
                     and_expire(1209600).
                     and_minimum(3600)
  end
end

その他の RR

基本的に Dnsruby::RR のドキュメントや infrataster-plugind-dnsrspec-dns の例を見ておけば、なんとなく雰囲気は察することができると思いますし、代表的な RR については例があるのでわかりやすいと思います。

最後に書きながらハマったところをメモがてら残しておきます。

DNSEC 周りの応答には Base64エンコードしたデータが含まれていますが、そのまま and_signature に渡すとテストに失敗します。Base64.decode64 で符号化を解いた状態でマッチャーに渡してあげましょう。コードの中身まで追っていませんが、Dnsruby の処理でそのようになっているような気がします。例えば、example.com の DNSKEY の1つは以下のように応答があります。

example.com.            3600    IN      DNSKEY  256 3 8 AwEAAblDjCPejMhknWXZqbwBEUPI6Lkwjvp0XlUNTBqW2glZrgf3MXjJ ZBXl8rhYoTkrov7jmbBaBOPTkqlQAbfOKFNoG+U+boGG6Zmy00l2XRP1 nckVMpJ2TxiDVcXJqs78MetC1Ztu4p6bj4VrJCYTmv3ZULSrWleMSWtv YXqY0S23

この場合は最後の公開鍵部分が Base64エンコードされているので、以下のようにするとテストできます。

describe dns('example.com.') do
  it 'DNSKEY RR' do
    is_expected.to have_entry.with_type('DNSKEY').
                     and_key(Base64.decode64('AwEAAblDjCPejMhknWXZqbwBEUPI6Lkwjvp0XlUNTBqW2glZrgf3MXjJZBXl8rhYoTkrov7jmbBaBOPTkqlQAbfOKFNoG+U+boGG6Zmy00l2XRP1nckVMpJ2TxiDVcXJqs78MetC1Ztu4p6bj4VrJCYTmv3ZULSrWleMSWtvYXqY0S23'))
  end
end

このようにあらかじめテストケースを網羅的に用意して、履歴を管理することで、思わぬ事故を防ぎ、家庭の平和(と父親の威厳)を守りたいものです。