AWS Tools for PowerShellでSSM Session Manager Pluginを使える様にしてみた

2023.04.14

しばたです。

AWS CLIと連携してSSM Sessionを介した機能を実現するSession Manager Pluginですが、現時点ではAWS Tools for PowerShellと連携することは出来ません。

この点に関して過去に上の記事を書いたりもしていたのですが、今回、AWS CLIやSession Manager Pluginの仕様を解析して無理やりAWS Tools for PowerShellと連携させることができたのでその内容を共用したいと思います。

現在の仕様

Session Manager Pluginを使うコマンドaws ssm start-sessionと同等のPowerShellコマンドレットはStart-SSMSessionになるのですが、現時点では、

Amazon Web Services CLI usage: start-session is an interactive command that requires the Session Manager plugin to be installed on the client machine making the call. For information, see Install the Session Manager plugin for the Amazon Web Services CLI in the Amazon Web Services Systems Manager User Guide.
Amazon Web Services Tools for PowerShell usage: Start-SSMSession isn't currently supported by Amazon Web Services Tools for PowerShell on Windows local machines.

とドキュメントにもある様に非サポートとなっています。

AWS CLIでaws ssm start-sessionを実行するとAWS環境上でSSM Sessionを生成し、そのセッション情報を元にSession Manager Plugin(session-manager-plugin)プロセスを起動するのですが、AWS Tools for PowerShellのStart-SSMSessionコマンドではセッション情報を返すことしかできません。

# AWS Tools for PowerShellではSession Manager Pluginと連動せずセッション情報を返すだけ
PS C:\> Start-SSMSession -Target i-0123456789abc

SessionId                                                   StreamUrl
---------                                                   ---------
aws-dotnet-sdk-session-xxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxx wss://ssmmessages.ap-northeast-1.amazonaws.com/v1/data-cha…

これはREST APIのStartSessionをシンプルに実行しているだけだからです。
AWS CLIではStartSession APIの実行に加えてsession-manager-pluginプロセスを実行する様に独自のカスタマイズが加えられており過去記事でも説明したとおりです。

session-manager-plugin の引数

そんなわけで、原理的にはどうにかしてsession-manager-pluginプロセスを起動してやればAWS Tools for PowerShellでも連携可能なはずです。
以前はこの点を解析しきれなかったのですが、現在はSession Manager Plugin自体がオープンソースとなりGitHub上でソースが公開されています。

このおかげでだいぶ解析がはかどりました。

Session Manger Pluginの引数は以下の様に6つ定義されています。

session.go

// ValidateInputAndStartSession validates input sent from AWS CLI and starts a session if validation is successful.
// AWS CLI sends input in the order of
// args[0] will be path of executable (ignored)
// args[1] is session response
// args[2] is client region
// args[3] is operation name
// args[4] is profile name from aws credentials/config files
// args[5] is parameters input to aws cli for StartSession api
// args[6] is endpoint for ssm service

各引数の仕様について簡単に説明していきます。

第1引数 : Sesson Response

第1引数にはStartSession APIを実行した結果のレスポンス(JSON)をそのまま全部渡してやります。
厳密にはレスポンス内にある

  • SSM Session ID : SessionId
  • Web Socket URL : StreamUrl
  • Session Token : TokenValue

さえあれば良い様です。

第2引数 : クライアントリージョン

第2引数はリージョン名(ap-northeast-1など)を渡してやります。

第3引数 : オペレーション名

第3引数にはREST APIのオペレーション名を渡してやります。
現状StartSession固定と考えて良いでしょう。

第4引数 : プロファイル名

第4引数には共有認証情報ファイル(AWS CLIで使う~/.aws/credentials)のプロファイル名を渡してやります。

Session Manager PluginはGo言語製のため.NETの認証情報は使えません。
幸いにもAWS Tools for PowerShellは.NETの認証情報だけでなく認証情報ファイルも使えるため、共有認証情報ファイルを使う設定であればそのプロファイル名を渡してやればOKです。

第5引数 : StartSession API パラメーター

第5引数にはStartSession APIを実行した際のパラメーターをJSON形式で渡してやります。
第3引数との関係を考えると「本来はREST API実行時パラメーター向けに汎用的なものを意図したのかな?」という感じですが、現在の実装はStartSession API専用となっておりTargetの値だけが実際に使われている感じです。

例えば最初に例示したStart-SSMSession -Target i-0123456789abcの場合であれば、

{"Target":"i-0123456789abc"}

といった感じの値を渡してやります。

第6引数 : SSMエンドポイント

最後の第6引数にはSSMサービスのエンドポイントURLを設定します。
例えば東京リージョンであれば以下の値を設定します。

https://ssm.ap-northeast-1.amazonaws.com

作った関数

ここまでの内容を踏まえて今回新たにStart-SSMSessionExという関数を作りました。
従来のStart-SSMSessionを拡張し、セッション情報取得後にsession-manager-pluginプロセスを起動しているだけです。

前提条件としては

  • Windows環境専用
    • 非Windowsでの動作確認をしてないため。とはいえ少しの改修で対応できる気はします。
  • AWS Tools for PowerShellインストール済み
    • 最低限 AWS.Tools.SimpleSystemsManagement モジュールが必要です。
  • Session Manager Pluginインストール済み
  • プロファイルを共有認証情報ファイルに記載すること
    • .NETの認証情報は使えません

となります。

function Start-SSMSessionEx () {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 1)]
        [string]$DocumentName,
        [Parameter(Mandatory = $false)]
        [hashtable]$Parameter,
        [Parameter(Mandatory = $false)]
        [string]$Reason,
        [Parameter(Mandatory = $true)]
        [string]$Target,
        [Parameter(Mandatory = $false)]
        [switch]$PassThru,
        [Parameter(Mandatory = $false)]
        [string]$ProfileName,
        [Parameter(Mandatory = $false)]
        [object]$Region
    )
    
    if (-not (Get-Command 'session-manager-plugin' -Type Application -ErrorAction Ignore)) {
        $message = @'
SessionManagerPlugin (session-manager-plugin) is not found.
Please refer to SessionManager Documentation here:
https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html
'@
        Write-Error $message
        return
    }

    $session = $null
    try {
        $params = @{
            Parameter   = $Parameter
            Target      = $Target
            ProfileName = $ProfileName
            Region      = $Region
        }
        if (-not [string]::IsNullOrEmpty($DocumentName)) {
            $params['DocumentName'] = $DocumentName
        }
        if (-not [string]::IsNullOrEmpty($Reason)) {
            $params['Reason'] = $Reason
        }
        $session = Start-SSMSession @params
    } catch {
        Write-Error $_
        return
    }
    if ($null -eq $session) {
        Write-Error 'Failed to get SSM session.'
        return
    }

    # Start SSM Session manager plugin
    if ([string]::IsNullOrEmpty($ProfileName)) {
        $ProfileName = $StoredAWSCredentials
    }
    if ([string]::IsNullOrEmpty($Region)) {
        $Region = (Get-DefaultAWSRegion).Region
    }
    # Setup arguments
    $arguments = @()
    # arg1 : session json
    $arguments += "`"$((($session | ConvertTo-Json -Compress) -replace '"', '\"'))`""
    # arg2 : region name
    $arguments += $Region
    # arg3 : StartSession
    $arguments += 'StartSession'
    # arg4 : shared credentials file profile nam
    $arguments += $ProfileName
    # arg5 : parameter json
    $arg5hash = @{ Target = $Target; }
    if (-not [string]::IsNullOrEmpty($DocumentName)) {
        $arg5hash['DocumentName'] = $DocumentName
    }
    if ($null -ne $Parameter) {
        $arg5hash['Parameter'] = $Parameter
    }
    if (-not [string]::IsNullOrEmpty($Reason)) {
        $arg5hash['Reason'] = $Reason
    }
    $arguments += "`"$(($arg5hash | ConvertTo-Json -Compress) -replace '"', '\"')`""
    # arg 6 : SSM endpoint URL
    $arguments += "https://ssm.$($region).amazonaws.com"
    Write-Verbose 'session-manager-plugin arguments'
    for ($i = 0; $i -lt $arguments.Count; $i++) {
        Write-Verbose "  arg$($i + 1) : $($arguments[$i])"
    }
    # start session-manager-plugin
    if ($PassThru) {
        # PassThru
        $proc = Start-Process -FilePath 'session-manager-plugin' -ArgumentList $arguments -PassThru -WindowStyle Hidden
        Write-Host ('Starting session with SessionId: {0}' -f $session.SessionId)
        Write-Host ('Start session-manager-plugin process ({0})' -f $proc.Id)
        return [PSCustomObject]@{
            Session = $session
            Process = $proc
        }
    } else {
        # Wait
        Start-Process -FilePath 'session-manager-plugin' -ArgumentList $arguments -Wait -NoNewWindow
    }
}

大体の処理は見たまんまという感じではありますが、Windows環境だと引数に渡すJSONファイルのダブルクオートをエスケープする("\"にする)必要がある点に結構ハマりました。

その他にはsession-manager-pluginの起動で終了待ちをするか否かで結構悩み、結局は-PassThruパラメーターを付けて選択できる様にしました。
デフォルトでは終了待ちする様にしましたが、終了待ちをせずにセッションIDだけ受け取り非同期にSSMセッションを終了することもできます。
ちなみにSession Manager Pluginの仕様としてはSSMセッションの終了を検知すると自動でプロセスも終了する様になっています。

実行例 : ポートフォワード

例として以下の様なパラメーターでこの関数を実行してやるとRDPのポートフォワードが可能になります。

# 33389 → 3389 ポートへのフォーワード
$params = @{
   Target       = 'i-01234567890abcdef'
   DocumentName = 'AWS-StartPortForwardingSession'
   Parameter    = @{portNumber = @('3389'); localPortNumber = @('33389') }
}
Start-SSMSessionEx @params

SSMセッション取得後にSession Manger Pluginを起動してポートフォワードの待ち受けをしてくれるので、あとはlocalhostへRDP接続してやるだけです。

おまけ : PSEC2RDPモジュール

本記事を書くきっかけとして、自分用にPSEC2RDPという名前のPowerShellモジュールを作って公開したのでおまけとして紹介します。

このモジュールではEC2インスタンスへのRDP接続を便利にするために以下の2つの関数を公開してます。

  1. 公開インスタンスに対してAdministratorパスワードの取得とRDP接続を自動で行う Start-EC2RDPClient 関数
  2. 非公開インスタンスに対してAdministratorパスワードを取得、SSMのポートフォワードを裏で実行してRDP接続を自動で行う Start-SSMRDPClient 関数

特に後者の「SSMのポートフォワードを裏で実行」を実現するためにSession Manager Pluginの仕様を改めて調べ直しました。

私個人としてはこのモジュールを便利に使えているのですが、「Session Manager Pluginの仕様を把握できた今ならGo言語かC#あたりで処理を書き直した方がより汎用的なツールを作れるな。」という気持ちになってしまったのでPowerShell Galleryでの公開まではしていません。
気になる方はリポジトリをクローンして試してみてください。

最後に

以上となります。

約三年半越しにリベンジできたので個人的には非常に嬉しいです。
万人向けの内容ではありませんが本記事の内容が誰かの役に立てば幸いです。