Dockerfile を書いている時イメージを軽量化するためにマルチステージビルド形式を採用することがしばしばあります。これはざっくりいうと「ビルド用」と「実行用」にイメージを分け、ビルドで使った不要なファイルやライブラリを実行用には持ち込まない構成です。こうすることで不要なファイルを捨てられて最終的なイメージが軽くなります。
この構成を作る時はこの実行用にコピーすべきファイルが何なのか調べるのが手間です。lddコマンドで共有ライブラリの依存関係を確認できますが、シンプルに結果の行が多かったり、依存先がシンボリックリンクで更に先を探索する必要があったり、複数ファイルに対してこれを行わなくてはならなかったり、といった具合です。この記事では、そういった時に使えるシェルスクリプトを紹介します
実際のコードが次です。
#!/bin/bash
# スクリプト全体を安全に実行するための設定:
# -e: エラーがあった時点で即終了
# -u: 未定義変数の使用をエラーにする
# -o pipefail: パイプライン内で1つでも失敗したら全体を失敗扱いにする
set -euo pipefail
show_help() {
cat <<EOF
使い方: $0 <実行ファイル1> [実行ファイル2] ...
指定した実行ファイルが依存する共有ライブラリを調べて一覧表示します。
依存する共有ライブラリがシンボリックリンクの場合は、リンクと実体のパスを表示します。
"not found" のライブラリがある場合は警告を表示します。
例:
$0 /usr/bin/java /usr/local/bin/mapserv
出力はユニークなパスの一覧です。
EOF
}
# 引数に --help または -h が含まれていた場合はヘルプを表示して終了
for arg in "$@"; do
case "$arg" in
--help|-h)
show_help
exit 0
;;
esac
done
# 引数が1つもない場合はエラーメッセージを表示して終了
if [ "$#" -eq 0 ]; then
echo "エラー: 実行ファイルを少なくとも1つ指定してください。" >&2
echo " $0 --help で使い方を表示できます。" >&2
exit 1
fi
# 一時ファイルを作成。スクリプト終了時に自動削除するよう trap を設定
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
# 渡された全てのバイナリについて処理を行う
for BIN in "$@"; do
# 指定されたファイルが存在しない場合は警告してスキップ
if [ ! -f "$BIN" ]; then
echo "警告: 実行ファイルが存在しません: $BIN" >&2
continue
fi
# lddコマンドで依存ライブラリ情報を取得し、awkで必要な情報だけを抽出
ldd "$BIN" | awk -v bin="$BIN" '
# "not found" の場合は NOT_FOUND:ライブラリ名:バイナリ名 の形式で出力
/=> not found/ {
gsub(/^[[:space:]]+/, "", $1)
print "NOT_FOUND:" $1 ":" bin
next
}
# 通常の "libxyz.so => /lib/..." 形式なら /lib/... 部分を出力
/=>/ {
if ($3 ~ /^\//) print $3
next
}
# " /lib64/ld-linux..." のような形式に対応
/^[[:space:]]*\// {
print $1
}
' | while IFS= read -r entry; do
# "NOT_FOUND:..." の行なら警告を出してスキップ
if [[ "$entry" == NOT_FOUND:* ]]; then
libname=$(echo "$entry" | cut -d':' -f2)
sourcebin=$(echo "$entry" | cut -d':' -f3-)
echo "警告: $sourcebin が依存しているライブラリ '$libname' が見つかりません(not found)" >&2
continue
fi
# シンボリックリンクを辿って、リンクと実体を全て収集
current="$entry"
max_symlinks=100 # 無限ループ防止のためのリンク上限
count=0
while [ -L "$current" ]; do
# シンボリックリンクを一時ファイルに記録
echo "$current" >> "$tmpfile"
# readlinkでリンク先を取得
next=$(readlink "$current")
if [[ "$next" != /* ]]; then
# 相対パスの場合は現在のディレクトリを基準に変換
next="$(dirname "$current")/$next"
fi
# 次のリンク先を絶対パスに解決
resolved=$(readlink -f "$next") || {
echo "警告: パス解決に失敗しました: $next(元: $entry)" >&2
break
}
current="$resolved"
# リンクをmax_symlinks以上辿ったら無限ループの可能性があるため中断
count=$((count + 1))
if [ "$count" -ge "$max_symlinks" ]; then
echo "警告: シンボリックリンクの辿りすぎ: $entry" >&2
break
fi
done
# 最後に実体ファイルが存在すれば記録
if [ -e "$current" ]; then
echo "$current" >> "$tmpfile"
else
echo "警告: 実体ファイルが存在しません: $current(元: $entry)" >&2
fi
done
done
# 一時ファイルに記録されたすべてのパスをユニークにソートして出力
sort -u "$tmpfile"
使い方は次の通りです。
FROM almalinux:10.0 AS builder
# ここで色々ビルド
# ビルド結果の依存ライブラリを抽出・コピーしてアーカイブする
# ↑のシェルスクリプトで/usr/local/bin/tgt_bin /usr/local/bin/tgt_2.fcgi /tmp/xxx/build/tgt_3.soの依存関係を抽出
# while 内で各パスのファイルを /tmp/libs 以下にファイル構造そのままでコピー
# tar で /tmp/libs を固める
RUN mkdir -p /tmp/libs && \
sh ./↑のシェルスクリプト.sh \
/usr/local/bin/tgt_bin \
/usr/local/bin/tgt_2.fcgi \
/tmp/xxx/build/tgt_3.so \
| while IFS= read -r path; do \
if [ -e "$path" ]; then \
dest_dir="/tmp/libs$(dirname "$path")"; \
mkdir -p "$dest_dir"; \
cp -av "$path" "$dest_dir/"; \
fi; \
done && \
tar -czf /tmp/deps.tar.gz -C /tmp/libs . && \
rm -rf /tmp/libs
### 実行ステージ
FROM almalinux:10.0 AS runtime
# ここで色々実行ファイル用の環境を整備
# builder から依存ライブラリセットをコピーして展開 && 掃除
# rsync を使ってディレクトリ構造を維持
COPY --from=builder /tmp/deps.tar.gz /tmp/deps.tar.gz
RUN mkdir -p /tmp/deps && \
tar -xzf /tmp/deps.tar.gz -C /tmp/deps && \
rsync -a /tmp/deps/ / && \
rm -rf /tmp/deps /tmp/deps.tar.gz
こんな感じで依存ライブラリをまとめてコピーでき楽ができます。このスクリプトを使ってDockerfileを作る時の注意点として ldconfig コマンドで共有ライブラリの情報の更新忘れ、ビルド結果そのもの(シェルスクリプトに渡したファイル)のコピー忘れがあります。一通りうまくいったけど何故か動かないとなったらこの辺りが原因になりがちです。