シェルスクリプトをマルチOS対応に設計する方法|RHEL・Ubuntu・Alpineを1本で動かすOS検出とパッケージ管理の抽象化

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)シェルスクリプト > シェルスクリプトをマルチOS対応に設計する方法|RHEL・Ubuntu・Alpineを1本で動かすOS検出とパッケージ管理の抽象化
「RHELで動かしたスクリプトをUbuntuにコピーしたら、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非依存にできる


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

なぜシェルスクリプトのマルチ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
この差分を1箇所(OS検出関数)に集約し、他の関数はOS非依存で書けるように設計するのがゴールです。

/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/"

Ubuntu 24.04 LTS での出力例:

$ 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

Alpine Linux 3.20 での出力例:

$ 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_FAMILYPKG_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

Alpine Linux 3.20 で実行した出力:

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を意識しなくて済む点です。

# どのOSでも同じ書き方(RHEL なら dnf、Ubuntu なら apt-get が呼ばれる) if ! is_pkg_installed curl; then echo "INFO: curl をインストールします" install_pkg curl fi

RHELでの実行結果(curl 未インストール時):

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

RHEL系では 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 }

Alpine Linux 3.20 での 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 "$@"

Ubuntu 24.04 LTS での実行結果:

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: セットアップ完了

RHEL 9.4 での実行結果(2回目実行・既インストール時):

INFO: OS_FAMILY=rhel INFO: nginx は既にインストール済みです INFO: セットアップ完了

同一スクリプトがOS切り替えなく動いていることが確認できます。

トラブルシュート:よくある移植失敗パターン

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-getyum を呼び出しているスクリプトが残っていると、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 をフォールバックで確認
マルチOS対応シェルスクリプトの核心は「OS検出を1回行い、その結果を変数に保存し、以降は変数だけ見る設計」です。detect_os をスクリプト先頭で呼び出すだけで、残りの関数はOSを意識せずに書けるようになります。複数のディストリビューションにまたがる自動化スクリプトを管理している現場では、ぜひ取り入れてみてください。シェルスクリプトを使った自動化や運用設計をさらに体系的に学びたい方は、下記の講座もあわせてご覧ください。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。