カスタムランタイムでPowerShell Lambdaを試してみた

2023.09.14

しばたです。

去年の話なのですが、AWSのラボからPowerShell向けのLambda Layerのサンプル実装が提供されており、カスタムランタイム上でPowerShellスクリプトの関数を実行することが可能になっています。

いずれ試そうと思いつつずっと放置してしまっていたのですが、最近Lambdaの記事を書いていたのもあり勢いでこいつも試してみました。

通常のPowerShell Lambdaとの違い

通常のPowerShell Lambdaはプログラムの実体が「PowerShellスクリプトを実行するC#プログラム」となっており、C#のアセンブリ一式がZipファイルにまとめられてデプロイされる形になっています。

このため

  • コードエディタでスクリプトの内容を参照・編集できない
  • 利用可能なPowerShellのバージョン(正確にはPowerShell SDKのバージョン)が.NETのバージョンに依存する

といった制約があります。
これらの制約を解消するため...だけでは無く、どちらかというと実験目的 *1で独自のLambda Layerを提供してカスタムランタイム上でPowerShell Lambdaが利用可能になりました。

この仕組みではLambda Layerに

  • PowerShell本体のバイナリ一式
  • bootstrapスクリプト (Runtime APIを直接操作するPowerShellスクリプト)
  • bootstrapスクリプトで使う専用モジュール

をまとめることでLambda関数の実行基盤にしています。
このため、利用者は

  • PowerShellスクリプトだけデプロイすれば良い
  • コードエディタでスクリプトの内容を参照・編集可能に

といったメリットを享受できる様になります。
加えてLambda LayerにPowerShell本体一式があるため、使用するPowerShellのバージョンを利用者が自由に選べることもメリットになります。

試してみた

前置きはこれくらいにして、あとは実際に試してみるのが手っ取り早いでしょう。

0. 前提条件

Lambda Layerは上述のGitHubリポジトリにあるソースから自分で作る必要があり、AWS SAMを使ってビルドとデプロイをする前提となっています。
このため、gitAWS SAM CLIが事前にインストールされている必要があります。
環境に応じて複数の方法でLambda Layerを作成可能となっており、環境によってはmakeコマンドやdockerが必要になる事もあります。

今回は私の開発環境(Windows 11)上で以下のプログラムがインストールされた環境で作業しています。

  • PowerShell 7.3.6
  • git 2.42
  • SAM CLI 1.97.0

1. PowerShell Lambda Layerのセットアップ

はじめにGitHubリポジトリからソース一式をCloneします。
任意のディレクトリでgit cloneを実行してください。

git clone https://github.com/awslabs/aws-lambda-powershell-runtime.git

リポジトリ内にはLambda Layer以外にデモ用スクリプト等も用意されています。
Lambda Layerはpowershell-runtimeフォルダにあるのでこのディレクトリまで移動しておきます。

# リポジトリのルートへ移動
cd .\aws-lambda-powershell-runtime\
# Lambda Layer一式は「powershell-runtime」にある
cd .\powershell-runtime\

Lambda Layerの具体的な作り方はこのディレクトリにあるreadme.mdに記載されており、ざっくり、

  • A) LinuxおよびWSL環境向け手順 (sam build)
  • B) Dockerを使った手順 (sam build)
  • C) 専用のPowerShellスクリプトで作成

の3パターン用意されています。
今回はWindows環境なのでC)の手順を採用し、このディレクトリにあるbuild-PwshRuntimeLayer.ps1スクリプトを実行します。

# 専用のPowerShellスクリプトでLambda Layerを作成
.\build-PwshRuntimeLayer.ps1

実行結果はこんな感じでエラー無く完了すればOKです。

このスクリプトにより.\Layersフォルダを作成の上Linux向けPowerShellのバイナリ一式等をダウンロードして保存します。
同時にカレントディレクトリにあるtemplate.ymlの内容を一部改変しビルド済みの体にします。

ちなみに、このスクリプトには2つの引数がありインストールするPowerShellのバージョンとアーキテクチャをそれぞれ設定可能です。

引数 内容 デフォルト値
-PwshVersion インストールするPowerShellのバージョン 7.2.13 (2023年9月時点)
-PwshArchitecture PowerShellのアーキテクチャx64 or arm64で選択 x64

たとえばArm環境向けのLayerを作りたい場合は以下の様にします。

# Arm環境向けに本日時点で最新のバージョン(7.3.6)をインストールする場合
.\build-PwshRuntimeLayer.ps1 -PwshArchitecture arm64 -PwshVersion 7.3.6

2. PowerShell Lambda Layerのインストール

この状態でsam deployコマンドを実行すると.\Layersフォルダの中身をZipにまとめてprovided.al2互換のLambda Layerを新規に作成してくれます。

# sam deploy でLambda Layerを新規作成 : samcofing.toml を環境に応じて設定しておいてください
sam deploy

デプロイに成功すればこんな感じでLambda Layerが出来上がります。

(今回は検証のため何度も作り直しているためバージョンが27になっています...)

3. Lambda関数の作成

あとはprovided.al2カスタムランタイムでこのLambda Layerを使った関数を用意してやるだけです。

今回はシンプルなスクリプトをAWS SAMで作成してやります。

source/function.ps1

#Requires -Version 7.0.0

<#
.SYNOPSIS
    Handler function
#>
function handler {
    [cmdletbinding()]
    param(
        [parameter()]
        $LambdaInput,
        [parameter()]
        $LambdaContext
    )
    Write-Output "Hello PowerShell!"
}

SAMのテンプレートはこんな感じ。

template.yaml

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: PowerShell-Lambda-Runtime sample Function.
Parameters:
  LayerName:
    Description: "Input lambda layer name."
    Type: String
    Default: "PwshRuntimeLayer"
  LayerVersion:
    Description: "Input lambda layer version."
    Type: Number
    Default: 1
    MinValue: 1
Resources:
  # Lambda function
  PowerShellFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: powershell-lambda-function
      Description: PowerShell lambda function.
      CodeUri: source/
      Runtime: provided.al2
      Architectures:
        - x86_64
      Handler: function.ps1::handler
      MemorySize: 512 # Minimum 512MB required 
      Timeout: 5
      Layers:
        - Fn::Sub: "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:layer:${LayerName}:${LayerVersion}"
      Environment:
        Variables: 
          POWERSHELL_RUNTIME_VERBOSE: TRUE # If set TRUE, output verbose logs.

上記テンプレートとスクリプトを適当なディレクトリに保存してsam deployしてください。

# Lambda Layerのバージョンは環境に応じた値を指定
sam deploy --parameter-overrides LayerVersion=27

エラー無く関数が作成されるとこんな感じになるはずです。

(コードエディタでスクリプトが参照可能)

(provided.al2カスタムランタイム + Lambda Layerの組み合わせ)

ちなみにランタイムに設定するハンドラはスクリプトファイル名::関数名の形式にするのが基本 *2です。

また、今回は動作確認用ということでPOWERSHELL_RUNTIME_VERBOSE環境変数を設定しています。
この環境変数がTrueに設定されている場合は詳細ログを出力します。

4. Lambda関数を試す

この状態で「Test」ボタンをクリックして動作確認するとスクリプト内のhandler関数が実行され「"Hello PowerShell!"」のレスポンスが返されます。

詳細ログを出す様にしてるのでランタイムの初期化処理でどの様な事が行われているかも読み取れるかと思います。

コードエディタも使えるのでその場でスクリプトを修正して再実行も可能です。

これは便利ですね。

ハマりポイント

ここまでの解説だとすべて順調に見えますが、実際には関数を正常終了させるまでかなりどハマりしました。

1. メモリ不足に注意

最初にこのカスタムランタイムを試した時に「最低限のスクリプトだしメモリサイズも最低で良いだろう。」と思いメモリサイズを最低の128MBに設定してました。

この状態で関数を実行すると一切のエラーコードを出すことなく関数が失敗します。

エラーコードが無いため原因を突き止めるのに苦労しました。
Lambda関数の実際のメモリ使用量からすると最低256MB程度あれば事足りそうな感じだったのですが、現実には512MB以上ないと安定した動作をしませんでした...

GitHub上のサンプルもメモリサイズが512MBなので最低512MB必要と考えておいた方が良さそうです。

2. C#ソースコードの Add-Type に失敗することがある

ある程度メモリサイズを確保した場合でもエラーコードを出さずに関数が失敗することがあり、詳細ログで以下のメッセージを出した直後に終了することがありました。

[RUNTIME-Set-LambdaContext]Importing .NET class from .cs file to support script properties and method

このログは初期化処理の内、LambaContext引数で使う型をC#のソースコードからAdd-Typeして定義する直前に出力されます。

Set-LambdaContext.ps1 より抜粋

    # Importing .NET class from .cs file to support the script property "RemainingTime" and method "getRemainingTimeInMillis".
    # This is taken from the Lambda .Net runtime LambdaContext code: https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs
    if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-Set-LambdaContext]Importing .NET class from .cs file to support script properties and method' }
    Add-Type -TypeDefinition ([System.IO.File]::ReadAllText('/opt/PowerShellLambdaContext.cs'))

エラーメッセージ等が一切出ていないもののAdd-Typeが何らかの理由で失敗しているのは明らかです。
(恐らくは前述のメモリ不足と関連していると予想されます)

私が検証した限りでは、このAdd-TypeをするタイミングをSet-LambdaContext関数で都度呼ぶのではなく、この関数が定義されているpwsh-runtimeモジュールの初期ロード時に実行する様に変えることでエラーが解消しました。

git diffで差分を取るとこんな感じの修正をしています。

diff.patch (git diffの結果)

diff --git a/powershell-runtime/source/modules/Private/Set-LambdaContext.ps1 b/powershell-runtime/source/modules/Private/Set-LambdaContext.ps1
index c521948..94d0ec2 100644
--- a/powershell-runtime/source/modules/Private/Set-LambdaContext.ps1
+++ b/powershell-runtime/source/modules/Private/Set-LambdaContext.ps1
@@ -12,11 +12,6 @@ function Private:Set-LambdaContext {
 
     if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-Set-LambdaContext]Start: Set-LambdaContext' }
 
-    # Importing .NET class from .cs file to support the script property "RemainingTime" and method "getRemainingTimeInMillis".
-    # This is taken from the Lambda .Net runtime LambdaContext code: https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs
-    if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-Set-LambdaContext]Importing .NET class from .cs file to support script properties and method' }
-    Add-Type -TypeDefinition ([System.IO.File]::ReadAllText('/opt/PowerShellLambdaContext.cs'))
-
     if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-Set-LambdaContext]Creating LambdaContext' }
     $private:LambdaContext = [Amazon.Lambda.PowerShell.Internal.LambdaContext]::new(
         $env:AWS_LAMBDA_FUNCTION_NAME,
diff --git a/powershell-runtime/source/modules/pwsh-runtime.psm1 b/powershell-runtime/source/modules/pwsh-runtime.psm1
index 0d8cff5..841fc6c 100644
--- a/powershell-runtime/source/modules/pwsh-runtime.psm1
+++ b/powershell-runtime/source/modules/pwsh-runtime.psm1
@@ -2,4 +2,10 @@
 # SPDX-License-Identifier: Apache-2.0
 
 Set-PSDebug -Strict
- # All Private modules merged into this file during build process to speed up module loading.
+
+# Importing .NET class from .cs file to support the script property "RemainingTime" and method "getRemainingTimeInMillis".
+# This is taken from the Lambda .Net runtime LambdaContext code: https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.Core/ILambdaContext.cs
+if ($env:POWERSHELL_RUNTIME_VERBOSE -eq 'TRUE') { Write-Host '[RUNTIME-pwsh-runtime.psm1]Importing .NET class from .cs file to support script properties and method' }
+Add-Type -TypeDefinition ([System.IO.File]::ReadAllText('/opt/PowerShellLambdaContext.cs'))
+
+# All Private modules merged into this file during build process to speed up module loading.

エラーが無くとも関数内で都度Add-Typeを呼ぶのは悪い実装なのでどうにかフィードバックしたいのですが、日本語でも説明が非常に面倒なので悩ましいです...

最後に

以上となります。

なかなか面白い仕組みでしたがこれを本番環境で使うかと言われたら私は使わないでしょう。
最初に実行するまでにかなりハマったのもあり、ある程度自力でトラブルシューティングできる人向けという印象です。

とはいえLambda Layerやカスタムランタイムの利用を学ぶ分には役立ったので学習用に試してみると良いと思いました。

脚注

  1. 提供元がAWS Labsなので...
  2. 他の指定方式もありますが本記事では触れません