Dockerコンテナでの設定ファイル生成にテンプレートとしてERBを使う

Dockerコンテナを起動する際に、環境変数で渡したパラメーターを使って設定ファイルを作りたいことがある。 例えばsambaにアクセスするとき、ユーザー名が foo なら許可、それ以外は不許可としたければ設定ファイルは以下のようにする。

[global]
        log file = /var/log/samba/%m

[share]
        path = /mount
        guest ok = no
        valid users = foo # ユーザー foo ならアクセスを許可する

このユーザー名を可変にしたいとしよう。Docker で docker run -e USER=foo -e PASSWORD=pass ... のようにしたいなら、以下のようになる。

Dockerfile

FROM ubuntu:latest

RUN apt update \
    && apt install -y \
    samba

COPY samba.sh /root/

EXPOSE 139 445

CMD ["/root/samba.sh"]

samba.sh

#!/bin/sh

# /etc/samba/smb.conf を生成する
cat <<SMBCONF >> /etc/samba/smb.conf
[global]
        log file = /var/log/samba/%m

[share]
        path = /mount
        guest ok = no
        valid users = $USER
SMBCONF

# システムに foo ユーザーを追加し、foo ユーザーとしてアクセスできるようにする
useradd $USER
cat <<PASSWORD | passwd $USER
$PASSWORD
$PASSWORD
PASSWORD
cat <<PASSWORD | smbpasswd -a -s $USER
$PASSWORD
$PASSWORD
PASSWORD

# samba サーバーを起動する
/usr/sbin/smbd -F -S --no-process-group

しかしこのファイル生成方法は、smb.conf のテンプレートがスクリプト中にあって気持ちが悪い。 こんなとき Ruby なら ERB が使えるのだが、インストールすると依存する複数のパッケージも入ってしまうし、テンプレートを使いたいだけなのにちょっと大げさだ。

# Ubuntu 19.10 の場合
$ root@f86b2af49215:/# apt install ruby
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following additional packages will be installed:
  ca-certificates fonts-lato javascript-common libgdbm-compat4 libgdbm6 libjs-jquery libruby2.5 libyaml-0-2
  openssl rake ruby-did-you-mean ruby-minitest ruby-net-telnet ruby-power-assert ruby-test-unit ruby-xmlrpc
  ruby2.5 rubygems-integration unzip zip
Suggested packages:
  apache2 | lighttpd | httpd gdbm-l10n ri ruby-dev bundler
The following NEW packages will be installed:
  ca-certificates fonts-lato javascript-common libgdbm-compat4 libgdbm6 libjs-jquery libruby2.5 libyaml-0-2
  openssl rake ruby ruby-did-you-mean ruby-minitest ruby-net-telnet ruby-power-assert ruby-test-unit
  ruby-xmlrpc ruby2.5 rubygems-integration unzip zip
0 upgraded, 21 newly installed, 0 to remove and 23 not upgraded.
Need to get 7568 kB of archives.
After this operation, 31.9 MB of additional disk space will be used.
Do you want to continue? [Y/n]

Ruby には軽量版の mruby がある。 素の mruby のバイナリは Ubuntu 19.10 なら apt install mruby でインストールできるが、これには ERB がバンドルされておらず利用できないので、代わりに mitamae を使うことにする。 プロビジョニングツールであり ERB を使ってテンプレートから設定ファイルを生成し配置する機能をもともと備えているし、シングルバイナリで 2.28MB (v1.10.5の場合)とコンパクトなので、インストールも手間がかからない。

Dockerfile

FROM ubuntu:latest

RUN apt update \
    && apt install -y \
    samba

# mitamae の latest の URL を指定する。
# 最新版のバイナリへのリダイレクトを伴うためか、変更がなくとも docker build の度に毎回ダウンロードが走るので、気になる場合は指定バージョンの URL を使う。
ADD https://github.com/itamae-kitchen/mitamae/releases/latest/download/mitamae-x86_64-linux /root/mitamae
RUN chmod +x /root/mitamae

COPY samba.sh mitamae /root/

EXPOSE 139 445

CMD ["/root/samba.sh"]

samba.sh

#!/bin/sh

(cd /root && mitamae local setup.rb)
# mitamae にはユーザー追加機能も任意のコマンド実行機能もあるので、`passwd` `smbpasswd` も `setup.rb` に切り出してしまう。

# samba サーバーを起動する
/usr/sbin/smbd -F -S --no-process-group

setup.rb

# /etc/samba/smb.conf 生成
template "/etc/samba/smb.conf" do
  variables(user: ENV["USER"])
end

# ユーザー追加
user ENV["USER"] do
  password ENV["password"]
end

execute "Add a samba user" do
  command <<-COMMAND.gsub(/^ */, "")
    cat <<PASSWORD | smbpasswd -a -s #{ENV["USER"]}
    #{ENV["PASSWORD"]}
    #{ENV["PASSWORD"]}
    PASSWORD
  COMMAND
end

mitamae/templates/etc/samba/smb.conf.erb

[global]
        log file = /var/log/samba/%m

[share]
        path = /mount
        guest ok = no
        valid users = <%= @user %>

テンプレート部を別ファイルに切り出せた。

また mitamae 化の他の利点として、実行時にシステムへの変更がわかりやすく差分表示されることを記しておきたい。

パラメーターを JSON にしてみる

mitamae は JSON をパースできる。パラメーターを JSON で渡すことを考えてみる。

param = JSON.parse(ENV["SMB_PARAM"])
user = param["user"]

template "/etc/samba/smb.conf" do
  variables(user: user["name"])
end

user user["name"] do
  password user["password"]
end

execute "Add a samba user" do
  command <<-COMMAND.gsub(/^ */, "")
    cat <<PASSWORD | smbpasswd -a -s #{user["name"]}
    #{user["password"]}
    #{user["password"]}
    PASSWORD
  COMMAND
end
$ smb_param=$(cat <<PARAM
{
  "user": {
    "name": "foo",
    "password": "pass"
  }
}
PARAM

$ sudo docker run --rm -p 139:139 -p 445:445 -e SMB_PARAM="$smb_param"

コード量をそれほど変えることなく JSON に対応することができた。 これにより

  • パラメーターを階層化でき、追加や削除、環境変数名前空間管理が楽 (プレフィックス付きの長い環境変数名にしなくていい)
  • 配列が扱える (上記の例だとユーザーを複数にするとき USER1, USER2, ... などとせず "user": [...] とできる)
  • 環境ごとにパラメーターをまとめて管理できる (それぞれの JSON ファイルを作ればいい)

などの利便性を得られる。

なお実装は nowlinuxing/docker-samba においてあるので、気になる方は参照されたし。