カスタムランタイムでPowerShell Lambdaを試してみた
しばたです。
去年の話なのですが、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を使ってビルドとデプロイをする前提となっています。
このため、gitとAWS 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で作成してやります。
#Requires -Version 7.0.0
<#
.SYNOPSIS
Handler function
#>
function handler {
[cmdletbinding()]
param(
[parameter()]
$LambdaInput,
[parameter()]
$LambdaContext
)
Write-Output "Hello PowerShell!"
}
SAMのテンプレートはこんな感じ。
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して定義する直前に出力されます。
# 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 --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やカスタムランタイムの利用を学ぶ分には役立ったので学習用に試してみると良いと思いました。