SSMドキュメント AWS-RunShellScript にシバン(Shebang)が必要か確認してみた

2022.07.02

しばたです。

AWS Systems Managerには指定したインスタンス上でシェルスクリプトを実行するSSMドキュメントAWS-RunShellScriptがあります。

このドキュメントのチュートリアルページを確認すると実行するスクリプトにシバン(Shebang)を記述している場合とそうでない場合があり、定義がまちまちであることに気が付きました。

本記事ではAWS-RunShellScriptでShabangを書くべきか否かについて調べてみました。

ドキュメント上の使い分け

AWS-RunShellScriptの定義を確認すると「説明」に

Run a shell script or specify the commands to run.

とあり、シェルスクリプトまたは特定コマンドを実行するためのものであると記載されています。

確かにチュートリアルページでも単一コマンドを実行する例とスクリプトを実行する例が記載されており、単一コマンドを実行する場合はShebang無し、スクリプトを実行する場合はShebang有りと使い分けられていました。

# 単一コマンド ifconfig を実行する例 (Shebangなし)
aws ssm send-command \
    --instance-ids "instance-ID" \
    --document-name "AWS-RunShellScript" \
    --comment "IP config" \
    --parameters commands=ifconfig \
    --output text

# Bashスクリプトを実行する例 (Shebangあり)
aws ssm send-command \
    --document-name "AWS-RunShellScript" \
    --targets '[{"Key":"InstanceIds","Values":["instance-id"]}]' \
    --parameters '{"commands":["#!/bin/bash","yum -y update","yum install -y ruby","cd /home/ec2-user","curl -O https://aws-codedeploy-us-east-2.s3.amazonaws.com/latest/install","chmod +x ./install","./install auto"]}'

公式な裏付けはありませんが、ドキュメントでは「Shebangは使い分けるべきもの」という方針の様です。

AWS-RunShellScriptの実装を調べてみた

続けてAWS-RunShellScriptの実装を調べてみました。
AWS-RunShellScriptの定義から実際にスクリプトを実行する部分を抜粋するとこんな感じです。

AWS-RunShellScript

{

  // ・・・省略・・・

  "runtimeConfig": {
    "aws:runShellScript": {
      "properties": [
        {
          "id": "0.aws:runShellScript",
          "runCommand": "{{ commands }}",
          "workingDirectory": "{{ workingDirectory }}",
          "timeoutSeconds": "{{ executionTimeout }}"
        }
      ]
    }
  }
}

これは具体的にはSSM Agentにおいてaws:runShellScriptプラグインを使い"{{ commands }}"に指定された内容を実行することを意味しています。

そしてSSM AgentのソースはGitHubにありますのでaws:runShellScriptプラグインのソースから本記事に関連しそうな部分を抜粋してみます。

runshellscript.go (から一部抜粋)

var shellScriptName = "_script.sh"
var shellCommand = "sh"
var shellArgs = []string{"-c"}

// NewRunShellPlugin returns a new instance of the SHPlugin.
func NewRunShellPlugin(context context.T) (*runShellPlugin, error) {
	shplugin := runShellPlugin{
		context: context,
		Plugin: Plugin{
			Context:               context,
			Name:                  appconfig.PluginNameAwsRunShellScript,
			ScriptName:            shellScriptName,
			ShellCommand:          shellCommand,
			ShellArguments:        shellArgs,
			ByteOrderMark:         fileutil.ByteOrderMarkSkip,
			CommandExecuter:       executers.ShellCommandExecuter{},
			IdentityRuntimeClient: runtimeconfig.NewIdentityRuntimeConfigClient(),
		},
	}

	return &shplugin, nil
}

細かい説明は端折りますがSSM Agentのaws:runShellScriptプラグインでは指定されたコマンドをローカルの_sript.shに保存しsh -c _sript.shを実行することで処理を実現しています。

# SSM Agentが内部で呼び出しているコマンド
sh -c "対象インスタンスに保存されたスクリプト (_sript.sh)"

たとえばLinuxインスタンスの場合、実際にはこんな感じの処理が実行されます。

# Linuxインスタンスの場合はこんな感じで実行される
sh -c /var/lib/amazon/ssm/<インスタンスID>/document/orchestration/<Run Command ID>/awsrunShellScript/0.awsrunShellScript/_script.sh

この処理は単一コマンド指定、シェルスクリプト指定の場合で区別されておらずどちらの場合も内容は同じです。

AWS-RunShellScriptではShabangを書くべきか?

ここまでの内容からAWS-RunShellScriptは最終的にsh -c _sript.shを呼び出していることがわかりました。

こちらの記事によると各シェルにおいてShebangが無い場合の挙動はシェル依存だそうです。

加えてshコマンドの実体はOS次第であり、例えばAmazon Linux 2ではsh = bashでありUbuntuならsh = dashです。

Shebangを付けない場合の挙動が完全に環境依存になっている以上、実運用においては常にShebangを書いたが安全です。

動作確認

最後に簡単に動作確認をしてみます。

今回は分かりやすい結果になるUbuntu 20.04(ami-0a054d6fbb2cc67ee)のEC2インスタンスで検証します。
CanonicalオフィシャルのAMIではデフォルトでSSM Agentがインストール済みなので適切な権限を持つIAMロールをアタッチする以外の事前作業はしていません。

Shebang無しの場合

まずはこんな感じでShebang無しで簡単なスクリプトを実行してみます。

readlink /proc/$$/exe
echo $0

このスクリプトでは現在の自分自身のプロセス名と引数の値を取る想定です。
実行結果は以下の通りdashシェルで実行されていることがわかります。

/usr/bin/dash
/var/lib/amazon/ssm/i-00e1a344031b8beb9/document/orchestration/62d43a2e-3579-4b86-8903-d35518777127/awsrunShellScript/0.awsrunShellScript/_script.sh

Shebangありの場合

続けてShebangを書いてBashを使うことを明示してみます。

#!/usr/bin/env bash
readlink /proc/$$/exe
echo $0
echo $BASH_VERSION

実行結果は以下の通りちゃんとBashで実行されています。

/usr/bin/bash
/var/lib/amazon/ssm/i-00e1a344031b8beb9/document/orchestration/364b28bb-d3ec-4f66-b007-97f7322fbdcb/awsrunShellScript/0.awsrunShellScript/_script.sh
5.1.16(1)-release

補足 : 他のシェルも使える

当たり前といえば当たり前ですがOSにデフォルトインストールされていない別のシェルも明示することができます。
たとえば追加でPowerShell 7をインストールした後で以下の様にShebangでPowerShellを明示してやればちゃんとPowerShellが実行されます。

#!/usr/bin/env pwsh
readlink /proc/$pid/exe
$PSScriptRoot 
$PSVersionTable.PSVersion.ToString()

こちらは結果のみ記載。

/opt/microsoft/powershell/7/pwsh

7.2.5

ただ、PowerShellでは呼び出し元スクリプトの拡張子が.ps1でない場合$PSScriptRootなどの実行情報を取得できない仕様があるため若干意図した動作になっていません。

実運用でPowerShellを使う場合は専用のAWS-RunPowerShellScriptドキュメントを使ってください。
このドキュメントはWindowsだけでなくLinuxやmacOS環境もサポート済みであり上記で取得できなかった$PSScriptRootの値もちゃんと取れます。

/opt/microsoft/powershell/7/pwsh
/var/lib/amazon/ssm/i-00e1a344031b8beb9/document/orchestration/25fa7026-925c-4993-a818-fb0af59fb973/awsrunPowerShellScript/0.awsrunPowerShellScript
7.2.5

(PowerShellを実行する場合はAWS-RunPowerShellScriptを使う方が良い)

結論

AWS-RunShellScriptも含めてシェルスクリプトを実行する際はちゃんとShebangを書きましょう。