社用 Mac や Windows を MDM(Mobile Device Management)で管理している組織なら、ほぼ必ずどこかで「スクリプトを全端末に配って実行させる」運用が走ります。設定の差分修復、セキュリティ要件のチェック、社内ツールのインストール、ログイン項目の整備など、 Jamf Pro、Kandji(現 Iru)、Mosyle、Microsoft Intune、JumpCloud のいずれを使っていても、最後は「シェルで何かを動かす」ところに行き着くのが実情です。
ところが「じゃあ何で書くのが最適か」を真面目に考えると、選択肢は意外と広く、それぞれにクセがあります。Bash で書けばプリインストールに乗るが冪等性を担保しにくい。Swift や Go でバイナリにすれば実行は速いが、配布物にコード署名と公証が必須になる。
本記事では、MDM 経由のスクリプト配布で実際に選択肢になる Bash / zsh、Swift、Go、そして Windows 側の PowerShell について、プリインストール状況・配布形式・ロギング・冪等性の観点から比較し、macOS / Windows それぞれの現実解を整理します。
MDM スクリプト配布に固有の制約を先に押さえる
言語比較の前に、MDM 経由配布で必ず効いてくる制約を 4 つ挙げておきます。これらを満たせない言語は、どんなに書きやすくても採用できません。
1. ランタイムをユーザー端末に勝手に追加できない
スクリプトは管理端末で実行されるので、Node.js のような後付けランタイムは「事前に MDM から配ってあるか」が前提になります。配ってないなら、スクリプト自身が「ランタイムが無ければ何もしない」か「ランタイムを取りに行く」のどちらかを選ぶ必要があります。プリインストールに乗っている言語ほど、配布の独立性が高いというのが基本です。
2. root 権限で動くことが多い
Jamf Pro のスクリプト実行も Intune の macOS シェルスクリプトも、デフォルトは root として走ります。sudo が要らない代わりに、ユーザーディレクトリに何かを書こうとするとオーナーが root になって権限事故を起こすので、誰の権限で動くかを意識した記述が前提になります。
3. exit code が運用ダッシュボードのシグナルになる
MDM 側のレポートは、ほとんどがプロセスの終了コードを「成功 / 失敗」のシグナルとして集計します。Bash の set -e や Swift / Go の exit(1) のような、終了コードを意図どおりに出せる言語であることが、運用ダッシュボードの精度に直結します。途中で例外を握りつぶして 0 で終わる実装が混ざると、失敗端末を取りこぼします。
4. 配布物にコード署名と公証が要る場合がある
macOS 上でバイナリを実行する場合、Gatekeeper と公証(Notarization)の対象になります。スクリプト(テキストファイル)はこの対象外ですが、Swift / Go のバイナリを配るなら署名・公証の CI を組まないと、配布した瞬間にユーザー端末で実行を弾かれる事故が起こります。
言語別の比較
代表的な選択肢を、上の 4 制約に照らして並べます。
| 言語 | macOS でのプリインストール | Windows でのプリインストール | 配布形式 | コード署名・公証 |
|---|---|---|---|---|
Bash (/bin/bash 3.2) | あり | なし(WSL は別) | 単一の .sh | 不要 |
zsh (/bin/zsh) | あり(macOS の既定シェル) | なし | 単一の .sh | 不要 |
| Swift(バイナリ) | コンパイラはなし、ランタイムは macOS に同梱 | なし | バイナリまたは PKG | 必要 |
| Go(バイナリ) | なし | なし | バイナリまたは PKG | macOS では必要 |
| PowerShell | なし | あり(5.1 系標準、7 系は別途) | 単一の .ps1 | 配布元によっては署名要 |
ここからは、それぞれの言語の現場で効くポイントを掘り下げます。
Bash / zsh は macOS MDM の事実上の標準
Bash と zsh は、MDM 経由スクリプト配布における事実上の標準です。Jamf、Kandji、Mosyle、Intune の公式テンプレートも、コミュニティで共有される MacAdmins コミュニティ のスクリプト集も、ほぼ全部がシェルスクリプトです。理由ははっきりしていて、追加ランタイムが要らず、pkg 化も .sh のまま MDM に貼り付けるのも自由に選べるからです。
ただし macOS の /bin/bash は GPLv3 を避ける目的でバージョン 3.2 のまま据え置かれています。連想配列、mapfile、coproc といった bash 4 以降の構文は素の状態では使えないので、書く側はそこを踏まないように気をつけます。macOS の既定ユーザーシェルは zsh ですが、root 実行のスクリプトであれば #!/bin/zsh でも #!/bin/bash でも動きます。チームの慣習に合わせて選んで構いません。
シェルの強みは、トラブルが起きたとき端末側で bash -x ./script.sh で再現確認ができることです。バイナリで配ると、デバッグ時にいったんローカルで再ビルドする工程が入るので、初動の速さがまったく違います。MDM の本番運用に乗せるとログだけが頼りになる場面が増えるので、「ユーザーの SSH 越しに即時再現できる」という性質は思っているより効きます。
弱点は、規模が大きくなったときの保守性です。500 行のシェルは構造化テストを書きにくく、エラー処理のばらつきも出やすい。set -euo pipefail をテンプレに入れる、shellcheck を CI で回す、ロガーは logger コマンドで unified logging に流す、といった足回りを揃えてようやく長期保守に耐えるレベルになります。
macOS 限定なら Swift が最有力
Swift は、macOS のみが対象になる場面では非常に有力です。理由は 3 つあります。
- Swift ランタイムは macOS にバンドルされており、配布物は単一バイナリで完結する
- Swift Argument Parser や System framework など、コマンドラインツール向けのライブラリが Apple 公式で揃っている
- SystemConfiguration、Security、CoreFoundation などの macOS フレームワークに直接触れるので、シェルから
defaultsやsystem_profilerを呼ぶよりも安定して情報が取れる
ハイブリッド検出(Wi-Fi の SSID、FileVault の状態、構成プロファイルの内容など)を MDM スクリプトで読み取るとき、Swift で書いた小さなバイナリは Bash + system_profiler 解析よりも壊れにくい、というのが現場感覚です。
代償として、配布物には Apple Developer ID による署名と公証が必須になります。さらに macOS のアクセス制御(TCC: Transparency, Consent, and Control)がフルディスクアクセスやスクリーンレコーディングを要求する処理を含む場合、PPPC ペイロードを MDM 側で別途配って許可しておかないと、root で動かしているのに動作だけ拒否される、という独特の挙動を踏みます。
「macOS 専用だが、シェルでは届かない API を叩きたい」という条件が立つときに、Swift は群を抜きます。逆にいうと、それ以外のケースでは配布のオーバーヘッドの方が大きく、最初の選択にはなりにくいです。
クロスプラットフォーム前提なら Go が最有力
Go は、macOS と Windows の両方に同じツールを配りたいときに第 1 候補になります。GOOS=darwin GOARCH=arm64 と GOOS=windows GOARCH=amd64 でクロスコンパイルが効き、ランタイムレスの単一バイナリが出てくるので、配布形態がシンプルです。MDM 管理者の間で定番の osquery、Munki の Managed Software Center 周辺ツール、Fleet のエージェントなど、エンドポイント側で動く管理系ツールに Go 製が多いのは、この配布特性の恩恵があるからです。
macOS では Swift と同様にコード署名と公証が必要で、Apple Silicon と Intel に両対応するならユニバーサルバイナリ化も視野に入ります。
弱点は、Bash で 30 行で済む処理を Go で書くと 200 行になる、というところです。冪等性チェックや小さな分岐を伴うレシピを Go で書くのは、コストに見合いません。「macOS と Windows の両方で同じ挙動を保証したい」「シェルでは扱いづらいバイナリ形式を読み書きしたい」「並行処理で多数の API を叩く」など、Go の強みが立つ条件で初めて採算が取れます。
PowerShell は Windows 管理の標準
PowerShell は、Windows 側 MDM の事実上の標準です。Intune Management Extension(PowerShell スクリプト)も JumpCloud のコマンドランも、Windows ターゲットなら基本は PowerShell です。Windows 10 / 11 には Windows PowerShell 5.1 がプリインストールされており、追加ランタイム不要で .ps1 を投入できます。
クロスプラットフォーム志向の PowerShell 7 は macOS にもインストール可能ですが、macOS の管理スクリプトを PowerShell に統一するメリットは現状ほぼなく、Windows は PowerShell、macOS は zsh / Bash、というのが業界標準の落としどころです。
Windows 配布で押さえておきたいのは実行ポリシーの扱いです。MDM から流すスクリプトは、-ExecutionPolicy Bypass で起動するのが定石です。署名済みスクリプトとして配るなら、コード署名証明書のローテーションも MDM 側で組み込んでおきます。
ワークロード別の現実解
ここまでの整理を、よくある MDM スクリプトの分類にマッピングします。
flowchart TD
Start[配布したいスクリプト] --> Q1{ターゲット OS は}
Q1 -- Windows --> PS[PowerShell 5.1 で .ps1 を配布]
Q1 -- macOS のみ --> Q2{macOS API を直接触る}
Q1 -- 両方 --> Go[Go バイナリ + PKG / MSI]
Q2 -- Yes --> Q3{単発ツールか常駐か}
Q2 -- No --> Bash[Bash / zsh で .sh]
Q3 -- 単発 --> Swift[Swift バイナリ + 署名公証]
Q3 -- 常駐 --> Daemon[launchd + Swift / Go バイナリ]
具体的なユースケースに当てはめると次のようになります。
- 設定の差分修復、
defaults write系の小さな修正、ログファイルの収集など: Bash / zsh - 構成プロファイルの内容検証、FileVault の状態確認、ハードウェア固有の判定など macOS API 寄りの処理: Swift
- macOS と Windows の両方で同じ社内ツールを配りたい場合: Go バイナリを PKG / MSI で
- Windows ドメインや Active Directory との連携が中心: PowerShell
「全部 Bash でいい」と「全部 Go バイナリにすべき」の中間に正解があり、組織が運用する MDM とエンドポイントの構成によって最適点はずれます。とくに macOS only の組織では Bash + Swift の二段構え、Windows と macOS が混在する組織では Go の比重が上がる、という傾向が顕著です。
配布形式の選び方
言語が決まったら、次は配布形式です。MDM の入口は大きく分けて 3 つで、それぞれ向き不向きがあります。
| 配布形式 | 向く言語 | 利点 | 注意点 |
|---|---|---|---|
| MDM のスクリプト機能(テキスト直貼り) | Bash / zsh / PowerShell | 反映が速い、変更履歴が MDM に残る | バイナリは載せられない、500 KB 程度の上限を持つ製品もある |
| PKG(macOS) / MSI(Windows) | Swift / Go バイナリ | バイナリ・補助ファイル・launchd plist をまとめて配布できる | 署名・公証 / 署名付き MSI のビルドパイプラインが要る |
| MDM のカスタムアプリ機能 | 任意 | 期待どおりの場所に配置を強制できる | MDM 製品ごとの API 差が大きく、移行時の負債になりやすい |
シェルスクリプトを .sh のまま貼るのが一番手軽ですが、長期で運用していくと「何 KB を超えると貼れなくなる」「配布履歴を別の git リポジトリで管理したい」といった要求が出てきます。そうなったら、シェルスクリプトでも PKG にラップしてバージョンを切るほうが、一貫した配布パイプラインに乗せやすいです。
ロギングと冪等性で揃えておくべき足回り
言語選定とは別に、MDM 配布スクリプトでは共通で押さえたい運用要件があります。3 つに絞って書いておきます。
1. ロギングは unified logging に流す
macOS では logger コマンドや os_log で Unified Logging System に流すのが標準です。/var/log/install.log を直接書くより、サブシステムとカテゴリで絞り込めて、log show --predicate での横断検索が効くので運用が劇的に楽になります。Windows なら Windows イベントログ です。
2. 冪等性をスクリプト先頭で意識する
MDM スクリプトは「何度実行されても同じ結果」を保証する書き方が前提です。if [ -f /Library/Foo/installed.flag ]; then exit 0; fi のような早期リターンを先頭に置く、defaults write の前に既存値を読む、Go なら os.Stat で対象の存在確認を入れる、といった習慣化が効きます。
3. ドライランフラグを付ける
CI と本番の両方で同じスクリプトを使えるよう、--dry-run フラグを最初から組んでおくと、配布前テストが楽になります。Swift Argument Parser や PowerShell の [switch] パラメータは、このフラグを宣言的に追加できるので、Bash よりも宣言性が高い言語の利点が出やすいポイントです。
まとめ
MDM 経由でスクリプトを配布するときの言語選定は、純粋な言語比較ではなく「ランタイムをどこから供給するか」「コード署名と公証の負担をどこまで許容するか」「壊れたときの再現コストをどこまで下げたいか」の 3 軸で決まる問題です。
実運用で繰り返し戻ってきている結論を 3 行でまとめると、次のとおりです。
- macOS に閉じた小〜中規模スクリプトは Bash / zsh が現状最強。
shellcheckとset -euo pipefailで土台を揃え、必要なら Swift バイナリを併用する - macOS と Windows の両方を相手にするなら Go が第 1 候補。バイナリ配布のオーバーヘッドを CI で吸収できるなら、シェル混在より長期保守が楽になる
- macOS API に踏み込みたいときは Swift で小さなバイナリに切り出す。シェルから
defaultsやsystem_profilerを叩くより壊れにくく、運用ログの再現性も上がる
社内の MDM 運用に「とりあえず Bash で書いてしまう」以上の選択肢を持っておくと、台数が増えてシェルでは届かない要件が増えてきたときの初動が速くなります。Swift と Go を「いざというときに引ける駒」として整備しておくのが、現実的な落としどころだと感じています。
以上、MDM 経由のスクリプト配布で言語選定に悩んでいる現場からお送りしました。