yumコマンドが見つからないとエラーになった」「OS別にほぼ同じスクリプトを3本管理していて、1箇所修正するたびに3か所直さなければならない」複数のLinuxディストリビューションが混在する環境では、パッケージ管理(yum/apt/apk)・サービス管理(systemctl/rc-service)・OS識別ファイルのパスまで微妙に異なります。この差分を吸収せずに書いたスクリプトは、環境が変わるたびに修正が必要になり保守コストが跳ね上がります。
この記事では、/etc/os-release を使ったOS種別の自動判定と、差分を吸収するbash関数の設計パターンを解説します。RHEL 9.4 / Ubuntu 24.04 LTS / Alpine Linux 3.20 の実機出力とともに、1本のスクリプトで複数OSに対応する方法を紹介します。
この記事のポイント
・/etc/os-release の ID でRHEL・Ubuntu・Alpine を自動判定できる
・detect_os 関数で OS_FAMILY にセットし以降は変数のみ参照する設計が定石
・install_pkg 関数でOS差分を吸収すると呼び出し側はOS非依存で書ける
・Alpineの rc-service も関数化してサービス管理をOS非依存にできる
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜシェルスクリプトのマルチOS対応設計が必要なのか
1. ディストリビューション混在は珍しくない
企業サーバーでは、歴史的な経緯でRHEL系とDebian/Ubuntu系が混在していることがよくあります。コンテナ環境ではAlpine Linuxが標準的なベースイメージとして広く使われ、さらにマルチディストリ環境が広がっています。こうした環境で「OSごとにほぼ同じスクリプトを複数本管理する」アプローチの問題点は3つです。
・保守コストの増加:ロジックを1箇所直すたびに全OS版を修正しなければならない
・漏れバグ:RHEL版は直したがDebian版を忘れる
・新OS対応の追加コスト:Rocky Linux / AlmaLinuxの普及に合わせて新たなコピーが増え続ける
2. 主要なOS差分の整理
マルチOS対応で意識すべき差分は3点です。| 項目 | RHEL / Rocky Linux | Ubuntu / Debian | Alpine Linux |
|---|---|---|---|
| パッケージインストール | dnf install -y |
apt-get install -y |
apk add --no-cache |
| パッケージ確認 | rpm -q パッケージ名 |
dpkg -l パッケージ名 |
apk info -e パッケージ名 |
| サービス起動 | systemctl start サービス名 |
systemctl start サービス名 |
rc-service サービス名 start |
| 自動起動設定 | systemctl enable サービス名 |
systemctl enable サービス名 |
rc-update add サービス名 default |
| /etc/os-release の ID | rhel / rocky / almalinux | ubuntu / debian | alpine |
/etc/os-release でOSを判定する仕組み
/etc/os-release はFreedesktop.orgが定めたOS識別ファイルで、RHEL・Ubuntu・Alpine を含む主要ディストリビューションが提供しています。スクリプトでOS判定に使う最も信頼性の高い方法です。RHEL 9.4 での出力例:
$ cat /etc/os-release NAME="Red Hat Enterprise Linux" VERSION="9.4 (Plow)" ID="rhel" ID_LIKE="fedora" VERSION_ID="9.4" PRETTY_NAME="Red Hat Enterprise Linux 9.4 (Plow)" ANSI_COLOR="0;31" CPE_NAME="cpe:/o:redhat:enterprise_linux:9::baseos" HOME_URL="https://www.redhat.com/"
$ cat /etc/os-release PRETTY_NAME="Ubuntu 24.04.2 LTS" NAME="Ubuntu" VERSION_ID="24.04" VERSION="24.04.2 LTS (Noble Numbat)" ID=ubuntu ID_LIKE=debian HOME_URL="https://www.ubuntu.com/" UBUNTU_CODENAME=noble LOGO=ubuntu-logo
$ cat /etc/os-release NAME="Alpine Linux" ID=alpine VERSION_ID=3.20.3 PRETTY_NAME="Alpine Linux v3.20" HOME_URL="https://alpinelinux.org/" BUG_REPORT_URL="https://gitlab.alpinelinux.org/alpine/aports/-/issues"
ID 変数です。RHEL系は rhel / rocky / almalinux、Debian/Ubuntu系は ubuntu / debian、Alpine は alpine と統一されています。スクリプトから読み込む際は
.(source の同義語)コマンドを使います。# /etc/os-release を読み込む(変数が現在のシェルに展開される) if [[ -f /etc/os-release ]]; then . /etc/os-release fi echo "OS: ${ID:-unknown} ${VERSION_ID:-}"
. で読み込むことで、ファイル内の変数がすべて現在のシェルに展開されます。export されていない変数も含めて取得できます。detect_os 関数:OS種別を変数にセットする設計
OS検出ロジックを関数にまとめ、OS_FAMILY・PKG_MANAGER のグローバル変数に設定します。スクリプト先頭で1回呼ぶだけで、後続のすべての関数から参照できるようになります。#!/bin/bash set -euo pipefail # グローバル変数(detect_os が設定する) OS_FAMILY="" PKG_MANAGER="" detect_os() { local os_id="" if [[ -f /etc/os-release ]]; then . /etc/os-release os_id="${ID:-}" elif [[ -f /etc/redhat-release ]]; then # RHEL 5/6 など /etc/os-release がない古い環境向けフォールバック os_id="rhel" elif [[ -f /etc/debian_version ]]; then os_id="debian" fi case "${os_id}" in rhel | rocky | almalinux | centos | fedora) OS_FAMILY="rhel" PKG_MANAGER="dnf" ;; ubuntu | debian | linuxmint) OS_FAMILY="debian" PKG_MANAGER="apt-get" ;; alpine) OS_FAMILY="alpine" PKG_MANAGER="apk" ;; *) echo "ERROR: 未対応のOS: ${os_id}" >&2 return 1 ;; esac echo "INFO: OS_FAMILY=${OS_FAMILY}, PKG_MANAGER=${PKG_MANAGER}" }
local os_id で一時変数をローカルに閉じ込め、判定結果だけをグローバルに書き出す構造にすることで、関数の副作用を最小化しています。RHEL 9.4 で実行した出力:
INFO: OS_FAMILY=rhel, PKG_MANAGER=dnf
INFO: OS_FAMILY=alpine, PKG_MANAGER=apk
パッケージ管理を関数で抽象化する設計
OS_FAMILY が設定されたら、パッケージ操作を抽象化した関数を用意します。# パッケージをインストールする(OS非依存) install_pkg() { local pkg="$1" [[ -z "${pkg}" ]] && { echo "ERROR: パッケージ名が未指定" >&2; return 1; } case "${OS_FAMILY}" in rhel) dnf install -y "${pkg}" ;; debian) apt-get install -y "${pkg}" ;; alpine) apk add --no-cache "${pkg}" ;; *) echo "ERROR: OS_FAMILYが未設定。detect_osを先に実行してください" >&2 return 1 ;; esac } # パッケージがインストール済みか確認する(OS非依存) is_pkg_installed() { local pkg="$1" case "${OS_FAMILY}" in rhel) rpm -q "${pkg}" &>/dev/null ;; debian) dpkg -l "${pkg}" 2>/dev/null | grep -q '^ii' ;; alpine) apk info -e "${pkg}" &>/dev/null ;; *) return 1 ;; esac } # パッケージをアンインストールする(OS非依存) remove_pkg() { local pkg="$1" [[ -z "${pkg}" ]] && { echo "ERROR: パッケージ名が未指定" >&2; return 1; } case "${OS_FAMILY}" in rhel) dnf remove -y "${pkg}" ;; debian) apt-get remove -y "${pkg}" ;; alpine) apk del "${pkg}" ;; *) echo "ERROR: OS_FAMILYが未設定" >&2; return 1 ;; esac }
# どのOSでも同じ書き方(RHEL なら dnf、Ubuntu なら apt-get が呼ばれる) if ! is_pkg_installed curl; then echo "INFO: curl をインストールします" install_pkg curl fi
INFO: curl をインストールします Last metadata expiration check: 0:14:32 ago on Wed Sep 23 10:41:07 2026. Dependencies resolved. ================================================================================ Package Architecture Version Repository Size ================================================================================ Installing: curl x86_64 8.6.0-11.el9_4 baseos 301 k Transaction Summary ================================================================================ Install 1 Package ... Installed: curl-8.6.0-11.el9_4.x86_64
rpm -q でインストール確認をしています。rpm コマンドの使い方も合わせて参照してください。パッケージ名がOS間で異なる場合の対処
Apache のパッケージ名はRHEL系がhttpd、Debian/Ubuntu系が apache2 と異なります。パッケージ名のマッピング関数を追加することで対応できます。# 正準名からOS固有のパッケージ名を解決する resolve_pkg_name() { local canonical="$1" case "${canonical}:${OS_FAMILY}" in apache:rhel) echo "httpd" ;; apache:debian) echo "apache2" ;; apache:alpine) echo "apache2" ;; # マッピングなければそのまま返す *) echo "${canonical}" ;; esac } # 使い方 pkg_name=$(resolve_pkg_name apache) install_pkg "${pkg_name}"
サービス管理の差分を吸収する設計
Alpine Linux は systemd の代わりに OpenRC を採用しているため、systemctl が存在しません。サービス操作も同じように抽象化します。# サービスを起動する(OS非依存) service_start() { local svc="$1" [[ -z "${svc}" ]] && { echo "ERROR: サービス名が未指定" >&2; return 1; } case "${OS_FAMILY}" in rhel | debian) systemctl start "${svc}" ;; alpine) rc-service "${svc}" start ;; *) echo "ERROR: OS_FAMILYが未設定" >&2; return 1 ;; esac } # サービスを停止する(OS非依存) service_stop() { local svc="$1" case "${OS_FAMILY}" in rhel | debian) systemctl stop "${svc}" ;; alpine) rc-service "${svc}" stop ;; esac } # 自動起動を有効化する(OS非依存) service_enable() { local svc="$1" case "${OS_FAMILY}" in rhel | debian) systemctl enable "${svc}" ;; alpine) rc-update add "${svc}" default ;; esac } # サービスが起動中か確認する(OS非依存) service_is_active() { local svc="$1" case "${OS_FAMILY}" in rhel | debian) systemctl is-active --quiet "${svc}" ;; alpine) rc-service "${svc}" status 2>/dev/null | grep -q started ;; esac }
service_start nginx 実行結果:* Starting nginx ... [ ok ]
実践:マルチOS対応セットアップスクリプトの全体像
これまでの関数を組み合わせた完全なスクリプト例です。RHEL・Ubuntu・Alpine でNginxをインストールし、サービス起動まで行います。#!/bin/bash # nginx-setup.sh — RHEL / Ubuntu / Alpine 対応セットアップスクリプト set -euo pipefail OS_FAMILY="" PKG_MANAGER="" detect_os() { local os_id="" if [[ -f /etc/os-release ]]; then . /etc/os-release; os_id="${ID:-}" elif [[ -f /etc/redhat-release ]]; then os_id="rhel" elif [[ -f /etc/debian_version ]]; then os_id="debian" fi case "${os_id}" in rhel|rocky|almalinux|centos|fedora) OS_FAMILY="rhel"; PKG_MANAGER="dnf" ;; ubuntu|debian) OS_FAMILY="debian"; PKG_MANAGER="apt-get" ;; alpine) OS_FAMILY="alpine"; PKG_MANAGER="apk" ;; *) echo "ERROR: 未対応のOS: ${os_id}" >&2; return 1 ;; esac echo "INFO: OS_FAMILY=${OS_FAMILY}" } install_pkg() { case "${OS_FAMILY}" in rhel) dnf install -y "$1" ;; debian) apt-get install -y "$1" ;; alpine) apk add --no-cache "$1" ;; esac; } is_pkg_installed() { case "${OS_FAMILY}" in rhel) rpm -q "$1" &>/dev/null ;; debian) dpkg -l "$1" 2>/dev/null | grep -q '^ii' ;; alpine) apk info -e "$1" &>/dev/null ;; esac; } service_enable() { case "${OS_FAMILY}" in rhel|debian) systemctl enable "$1" ;; alpine) rc-update add "$1" default ;; esac; } service_start() { case "${OS_FAMILY}" in rhel|debian) systemctl start "$1" ;; alpine) rc-service "$1" start ;; esac; } main() { detect_os # Ubuntu / Debian はパッケージリストを最新化してからインストール if [[ "${OS_FAMILY}" == "debian" ]]; then apt-get update -y fi if ! is_pkg_installed nginx; then echo "INFO: nginx をインストールします" install_pkg nginx else echo "INFO: nginx は既にインストール済みです" fi service_enable nginx service_start nginx echo "INFO: セットアップ完了" } main "$@"
INFO: OS_FAMILY=debian Hit:1 http://jp.archive.ubuntu.com/ubuntu noble InRelease ... INFO: nginx をインストールします Reading package lists... Done Building dependency tree... Done ... Setting up nginx (1.24.0-2ubuntu7.1) ... INFO: セットアップ完了
INFO: OS_FAMILY=rhel INFO: nginx は既にインストール済みです INFO: セットアップ完了
トラブルシュート:よくある移植失敗パターン
1. /etc/os-release が存在しないコンテナや古いOSでNGになる
RHEL 6.x やコンテナの最小イメージ(scratch ベース等)では/etc/os-release が存在しないことがあります。detect_os 内のフォールバック確認が機能します(上記サンプルに実装済み)。識別ファイルの存在を事前確認するコマンド:
ls -la /etc/os-release /etc/redhat-release /etc/debian_version 2>/dev/null \ || echo "識別ファイルなし"
2. set -e 環境で is_pkg_installed が意図せずスクリプトを終了させる
is_pkg_installed は「インストールされていない場合は終了コード1を返す」仕様です。set -euo pipefail が有効な環境では、if 文や || で明示的に失敗を受け取らないとスクリプトが即座に終了します。# NG: is_pkg_installed が 1 を返した時点でスクリプトが終了する is_pkg_installed curl install_pkg curl # OK: if で失敗を受け取る(set -e でも問題なし) if ! is_pkg_installed curl; then install_pkg curl fi
3. パッケージ名がOS間で異なることに気づかず失敗する
Apache(httpd/apache2)や OpenSSH サーバー(openssh-server/openssh)のように、OSによってパッケージ名が変わるものがあります。スクリプトが特定OSでのみ失敗する場合はパッケージ名の差異を疑いましょう。# OS別のパッケージ名を一覧で確認する(RHEL系) dnf info パッケージ名 2>/dev/null | grep -E '^Name|^Summary' # (Ubuntu/Debian系) apt-cache show パッケージ名 2>/dev/null | grep -E '^Package|^Description'
4. ハードコードされたパッケージマネージャが残っている
ライブラリ化した関数を使わず直接apt-get や yum を呼び出しているスクリプトが残っていると、Alpine 環境でエラーになります。grep で一括検索して洗い出しましょう。# プロジェクト内でハードコードされたパッケージマネージャを検索する grep -rn 'apt-get\|yum install\|apk add' scripts/ --include='*.sh'
本記事のまとめ
| やりたいこと | 方法 |
|---|---|
| OS種別を自動判定する | . /etc/os-release で読み込み $ID を case 文で分岐 |
| 判定結果を全関数で共有する | detect_os でグローバル変数 OS_FAMILY に設定、以降は変数のみ参照 |
| パッケージ管理を抽象化する | install_pkg / is_pkg_installed 関数にOS分岐を集約 |
| Alpine のサービス管理に対応する | service_start / service_enable 関数で rc-service / rc-update を吸収 |
| パッケージ名がOS間で違う場合 | resolve_pkg_name 関数でマッピングテーブルを管理する |
| /etc/os-release がない古い環境 | /etc/redhat-release / /etc/debian_version をフォールバックで確認 |
detect_os をスクリプト先頭で呼び出すだけで、残りの関数はOSを意識せずに書けるようになります。複数のディストリビューションにまたがる自動化スクリプトを管理している現場では、ぜひ取り入れてみてください。シェルスクリプトを使った自動化や運用設計をさらに体系的に学びたい方は、下記の講座もあわせてご覧ください。
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:シェルスクリプトでmysqldumpの自動バックアップを設計する方法|世代管理・エラートラップ・ローテーション実装パターン
- この記事の属するカテゴリ:シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます