Share on

投稿日:
更新日:

Shell Script ベストプラクティス

はじめに

Shell Script は UNIX 系システムにおいて高度な自動化を実現するための非常に強力なツールで、トイル(Toil)の撲滅 に繋がります。

トイルとは、反復的で非創造的な作業のことを指します。 これには、例えば、手動でのシステムのスケーリングや、エラーのトラブルシューティング、ルーチンメンテナンス等が含まれます。 トイルを特定し、それを自動化することで、エンジニアはより創造的なタスクやプロジェクトに焦点を合わせることができます。

O'Reilly の Site Reliability Engineering 本によれば、トイルを判別する方法として、次のような基準が挙げられています。

oreilly-site-reliability-engineering.png
基準概要
1. 手作業である自動 or 半自動スクリプトでも手動で起動する場合はトイルに該当
2. 繰り返し発生する数回ではなく、定期的・継続的に発生
3. 自動化が可能スクリプトやツールで代替できる
4. 戦術的である想定外の対応や技術的負債の対応等、戦略とは無関係なタスク
5. 長期的な価値が無い一時的な解決策で、サービスの品質や拡張性に貢献しない
6. サービスの成長と共に増える利用者やトラフィックの増加に比例して作業が肥大化する( O(N)O(N) )

上記の項目に該当する作業は、Shell Script による自動化によって効率よく軽減することが可能です。

今回のブログでは、Shell Script を使った効率的な自動化の実践方法と、トイルを削減するベストプラクティスについて、Google から提供されている Shell Style Guide を参考に紹介したいと思います。

Shell Script とは

Shell Script とは、UNIX/Linux 等の OS において Shell 上で実行するコマンドを、1 行ずつ記述・実行できる「自動化されたプログラムファイル」のことです。

主に bash(Bourne Again Shell)等の Shell 環境で動作し、システム操作を自動化できます。

構造と基本的な構文

シェルスクリプトの基本構成は以下のようになっています。

#!/bin/bash を Shebang(シバン)と呼び、実行 Shell をカーネルに命令します。

スクリプトファイル(例:myscript.sh)に上記の内容を書き込み、以下のように実行します。

シェルスクリプトでできること

シェルスクリプトは、主に次のような処理に用いられます。

用途具体例
ファイル操作ファイルの作成・コピー・削除・移動
システム管理ユーザ管理、サービスの監視や起動・停止
バックアップ・メンテナンス処理一定期間毎にログを保存・古いログを削除
ネットワークタスクping, curl, scp を自動で定期実行
バッチ処理一括ユーザ登録、CSV データ処理
CI/CD の自動化(DevOps)デプロイスクリプト(ビルド → テスト → 配信)

なぜ Shell Script で自動化するのか

適切に設計された Shell Script によって、日々の手作業を排除し、ヒューマンエラーを減らし、作業の再現性を確保できます。

  • コマンドラインツールとの高い親和性
  • ファイル操作、サービス制御、ログ処理等に適している
  • 学習コストが低い
  • 多くの UNIX 系システムでデフォルトサポートされている

また、Google では以下のような理由から、Shell Script を書く場合は必ず Bash(#!/bin/bash)を使用するようです。

  • 全てのマシンに Bash が標準搭載されている
  • Bash 独自機能(配列・正規表現・[[ ]] 構文等)を活用できる
  • POSIX 互換を意識する必要が無い

スクリプトの基本構成

  • 他のユーティリティの呼び出しが主な役割(ラッパスクリプト等)
  • 100 行以下で完結できる
  • 高速性が求められない場合

複雑な制御フローや大規模なロジックとなった場合は、Go や Python 等、より構造化された言語への移行が推奨されています。

また、セキュリティ上の理由から Shell Script での SUID/SGID の利用を禁止し、代わりに sudo を使用することが推奨されています。

ここで、SUID(Set User ID)/ SGID(Set Group ID)とは、Unix 系の OS における特殊なファイル実行権限の一種です。 SUID/SGID は強力な特権を持つため、誤用すると深刻なセキュリティ問題を引き起こします。 さらに、権限昇格(Privilege Escalation)の原因となる脆弱性の温床にもなります。

Shell Style Guide by Google

Google の Style Guide で紹介されている Tips をいくつか紹介したいと思います。

コメントルール

ファイル冒頭

スクリプトの先頭に簡単な説明コメントを記述します。

関数コメント

関数コメントは次の形式で記述します。

実装に関するコメント

複雑な処理や読みづらいロジックにはコメントを追加します。

TODO コメント

改善予定箇所には、書いた本人の名前付きで TODO を追記します。

書式とフォーマット

インデント

インデントは 2 スペースで統一 し、タブは使用しません。

行の長さ制限

1 行は 80 文字以内が原則 で、長い文字列やコマンドはヒアドキュメントや改行 \ を使って複数行に分割します。

パイプ処理の書き方

1 行に収まらない場合は、パイプ毎に改行します。

制御構文の書き方

then や do は同じ行に記述し、fi や done は縦に揃えます。

コーディング実践ルール

変数の展開と引用

  • 必ず "${var}" のようにクォートしてバグを回避します。
  • $* の代わりに "$@" を使用します。
  • 特殊変数($?, $#, $$ 等)も基本はクォートします。

[[ ]] の使用

条件判定には、パス展開や空白分割を防げるため [...] の代わりに [[...]] を使用します。

文字列テスト

文字列が空かどうかのテストには -z または -n を使用します。

配列の使用

引数や要素のリストには配列を使用します。

また、文字列結合でのリスト生成は非推奨です。

関数と変数の命名規則

関数名

『スネークケース(小文字 + アンダースコア)』を使用します。

グローバル変数・定数

『すべて大文字 + アンダースコア』で記述します。

また、readonly や export を使って明示的に定義します。

ローカル変数

関数内では local を使ってスコープを限定します。

コマンド実行とエラーハンドリング

コマンド実行後には結果コードで成功/失敗を明示的に判定します。

パイプ処理では PIPESTATUS を使って各工程のステータスを確認できます。

一貫性を保つことの重要性

Shell Script に限らず、コーディングをする上で最も重要なのは「一貫性」です。 異なるコーディングスタイルが混在すると、保守の手間やバグの原因に繋がります。

  • 同じスクリプト内でスタイルを統一する
  • チームでスタイルガイドを共有する
  • 自動検査ツール(Shell Check 等)を導入する

例えば、VSCode では shell-format のようなフォーマッタツールも用意されています。

推奨される書き方

Sreake の記事を参考に、Shell Script で推奨される書き方(Tips)をいくつかまとめたいと思います。

エラーハンドリング

1. エラーで即座に停止

set -e を使用することでスクリプトを、その時点で即座に終了させることができます。

set -e を使用すると、スクリプトはエラーが発生した時点で即座に停止します。

また、set -o pipefail を追加することでパイプラインの一部でエラーが発生した場合も確実にスクリプトを停止させることができます。

2. トラップを使用したクリーンアップ

trap コマンドを使用することで、スクリプトが終了する際(正常終了でもエラー終了でも)に特定の処理を実行することができます。 これは一時ファイルの削除等のクリーンアップ処理に特に有用です。

3. 構造化ログの実装

何が起こったのかを知るためにはログが重要になります。 構造化ログを実装することで、イベントの詳細を効率的に記録し、分析できます。 日時、ログレベル、メッセージを含む一貫したフォーマットを使用し、JSON 等の機械可読形式で出力することで、ログの検索や集計が容易になります。

構造化ログを使用することで、ログの解析や問題の追跡が容易になります。

4. 再実行可能なスクリプトを書く

冪等性のあるコードを書くことが重要です。 これは、スクリプトを何度実行しても、同じ結果が得られることを意味します。

例えば、ファイルの作成やディレクトリの設定等、システムの状態を変更する操作を行う場合、既に目的の状態になっているかどうかを最初に検証します。 これにより、不要な処理を避け、エラーを防ぐことができます。

また、スクリプトの途中で失敗した場合でも、再実行時に問題なく続きから処理を行えるようになります。 冪等性を意識することで、より信頼性の高い、メンテナンスしやすいスクリプトを作成することができます。

  • ディレクトリの作成をする時

この関数は、ディレクトリが存在しない場合のみ作成を行います。 これにより、スクリプトを何度実行しても安全です。

  • パッケージのインストールをする時

この関数は、パッケージがまだインストールされていない場合のみインストールを行います。

パフォーマンスの最適化

1. ループの最適化

スクリプトでのループ最適化には、一般に以下の 2 つの方法があります。

  • 方法 1:seq コマンドを使用
  • 方法 2:Bash の算術式を使用

seq コマンドと Bash の算術式はケースバイケースでパフォーマンスが変動する可能性があるため、ループの最適化には以下の点を考慮することが重要です。

  • 実環境での測定の重要性
  • コンテキストと具体的なユースケースの考慮
  • 大規模データを扱う際の別アプローチ(例:ストリーミング処理)の検討
  • ループ構造だけでなく、ループ内の処理も含めた全体的な最適化

一部のケースでは、Bash の算術式よりも、seq コマンドの方がパフォーマンスが良いとされる例も紹介されています。

これは次のような理由によるものと考えられます。

  • seq コマンドの最適化された実装
  • Bash の算術演算の相対的な遅さ
  • メモリとプロセス生成のトレードオフ
  • システムの特性(CPU キャッシュ、メモリ管理等)

スクリプトの最適化は非常に複雑で、直感に反する結果をもたらすことがあります。 常に具体的なユースケースに基づいてベンチマークを取り、その結果に基づいて最適化を行うことが重要です。

2. パイプラインの使用

大きなファイルを処理する場合、パイプラインを使用することでメモリ使用量を抑えつつ効率的に処理することができます。

セキュリティの考慮

1. 入力のサニタイズ

サニタイズとは、ユーザまたは外部データを安全に処理できるようにクリーンな状態に変換(無害化)することです。

ユーザ入力を処理する際は、常に入力をサニタイズし、潜在的な悪意のある入力を防ぐことが重要です。

2. 変数の適切な引用

変数を適切に引用することで、予期せぬ動作や潜在的なセキュリティリスクを回避できます。

3. 最小権限の原則

最小権限の原則(PoLP:Principle of Least Privilege) とは、ユーザやアプリケーション、システムが特定のタスクを遂行するために必要なアクセス権限のみを付与するセキュリティ対策です。

スクリプトが root 権限で実行される場合、必要最小限の操作のみを root で行い、それ以外は一般ユーザ権限で実行するようにします。

4. 一時ファイルの安全な作成

mktemp コマンドを使用すると、安全に一時ファイルを作成できます。

また、trap コマンドを組み合わせることでスクリプト終了時に確実に一時ファイルを削除できます。

クロスプラットフォームの考慮

1. 可搬性のある Shebang

#!/bin/bash の代わりに #!/usr/bin/env bash を使用することで、異なるシステムでも bash のパスを正しく特定できます。

2. コマンドの挙動やオプション解釈に気をつける

Shell(sh, bash, zsh, fish 等)や、コマンドの種別(Bash 組み込みコマンド等)で、文字コードの解釈が異なる場合があります。 特に常用される echo, awk, sed コマンドは BSD 系 と GNU 系で実行時の文字解析・構文解析が異なるため、注意が必要です。

例えば、echo コマンドは、改行の解釈が macOS(BSD 版)と Linux(GNU coreutils 版)で異なります。

coreutils 版は -e で改行を明示します。

-e オプションは BSD 版では使用できません。

対応策として、POSIX に準拠した printf コマンドを使用することで、Bash や FreeBSD、busybox 等すべての環境で一貫した動作を保証します。

このように、コマンドによってはオプションの指定方法で挙動が変わるものがあるため、実行環境を考慮して実装することが重要です。

3. OS 依存の処理

uname コマンドを使用して実行環境を判別し、OS 固有の処理を適切に分岐させることができます。

テストとデバッグ

1. ユニットテストの導入

シンプルなユニットテストを導入することで、スクリプトの信頼性を向上させることができます。

2. デバッグモード

環境変数 DEBUG を設定することで、スクリプトの実行過程を詳細に追跡することができます。

バージョン管理ツールとの統合

1. GitHub Webhook の活用

GitHub の pre-commit フックを使用して、コミット前に自動的に Shell Script の構文チェックと静的解析を行うことができます。

  • .git/hooks/pre-commit に配置

ケーススタディ

1. ログ解析とレポーティング

Apache のアクセスログを解析し、日次レポートを生成するスクリプトの例です。

このスクリプトは Apache のアクセスログを解析し、総アクセス数、ユニーク訪問者数、最もアクセスの多いページ等の情報を含む日次レポートを生成します。

crontab を設定しておけば、毎日のレポート生成を自動化することができるため、トイルの削減に繋がります。

2. 定期的なバックアップ

特定のディレクトリにおいて、一定期間以内に作成・更新されたファイル(ここでは画像イメージ)を、探索し、.tar.gz 形式で指定ディレクトリに保存し、古いファイルを削除するスクリプトの例です。

こちらも、crontab を設定しておくことで、定期的なファイルのバックアップタスクを自動化することができます。

まとめ

今回のブログでは、Google の Style Guide を参考に Shell Script の推奨される書き方を紹介しました。

Shell Script による高度な自動化は、エラーハンドリング、パフォーマンス最適化、セキュリティ考慮、クロスプラットフォーム対応、テストとデバッグ、バージョン管理ツールとの統合等、多くの要素を考慮する必要があります。

これらのベストプラクティスとパターンを適用することで、より信頼性が高く、より保守・運用のしやすい効率的な自動化スクリプトを作成することができます。

参考・引用