PowerShellでAWS Lambdaを試してみた

しばたです。

去年の秋口に発表されたPowerShellでAWS Lambda、すでに色々な人が試してブログ記事など公開されていますが、Developers.IOではまだ記事が無いのとPowerShellおじさんとして試しておかないといけないと思いこの記事を書きます。

公式ドキュメント

参考となるドキュメントは以下にあります。

本記事では上記ドキュメントの内容をかいつまんで試していき、この他に補足となる情報をいくつかお伝えします。

開発環境のセットアップ

PowerShellでLambdaを試すにはクライアントとなる環境に以下のツールをインストールする必要があります。

1. PowerShell Core 6.x

Lambdaで使えるのはPowerShell Coreです。Windows PowerShellは使えません。

ドキュメントではPowerShell Core 6.0と記載されているものが多いですが、2019年2月3日現在、LambdaではPowerShell Core 6.1が使われているためクライアントにも最新のPowerShell Core 6.1をインストールしておけば良いでしょう。
本記事では最新版であるPowerShell Core 6.1.2を使用します。

インストール手順はGitHubのドキュメントを参考にしてください。

ちなみに私は自作のスクリプトを使って一発インストールしています。
よろしければ使ってみてください。

2. AWSLambdaPSCore モジュール

関数のテンプレートやパッケージの作成とデプロイを行うためにAWSLambdaPSCoreモジュールをインストールします。
PowerShell Coreを起動し、Install-Moduleでインストールできます。

Install-Module -Name AWSLambdaPSCore -Scope CurrentUser

3. .NET Core SDK 2.1

理由は後述しますが関数パッケージを作成するために.NET Core SDKのインストールが必要です。
SDKのバージョンはPowerShellのバージョンと合わせる必要があり、PowerShell Core 6.0~6.1は.NET Core 2.1製であるため、.NET Core SDK 2.1をインストールする必要があります。

.NET Core 2.1のダウンロードサイトから環境に応じたSDKをインストールしてください。

本記事ではSDK 2.1.503で試しています。

4. (Optional) AWSPowerShell.NetCore モジュール

意外に思われるかもしれませんがPowerShellでLambdaを扱うのにAWS Tools for PowerShell(AWSPowerShell.NetCoreモジュール)は必須ではありません。
このモジュールを使わない関数を作って利用することも可能です。(どこまで利用価値があるかは怪しいですが...)

ただし、AWSLambdaPSCoreモジュールの機能をつかってデプロイをする際はAWSPowerShell.NetCoreモジュールの機能(Get-AWSCredentials)が必要なためこのモジュールをインストールしておく必要があります。

AWS Tools for PowerShellはInstall-Moduleからインストール可能です。

Install-Module -Name AWSPowerShell.NetCore -Scope CurrentUser

他にエディタなどはお好みでインストールしてください。
以上で開発環境のセットアップは完了です。

Lambda関数を作ってみる

ここから実際に関数を作ってデプロイしていきます。
関数は手作業でも作れますが、AWSLambdaPSCoreモジュールの機能でテンプレートを生成できますので、本記事ではテンプレートから関数を生成していきます。

テンプレートの取得

Get-AWSPowerShellLambdaTemplateコマンドレットで関数テンプレートの一覧を取得できます。

PS C:\> Get-AWSPowerShellLambdaTemplate

Template                     Description
--------                     -----------
Basic                        Bare bones script
CloudFormationCustomResource PowerShell handler base for use with CloudFormation custom resource events
CodeCommitTrigger            Script to process AWS CodeCommit Triggers
DetectLabels                 Use Amazon Rekognition service to tag image files in S3 with detected labels.
KinesisStreamProcessor       Script to be process a Kinesis Stream
S3Event                      Script to process S3 events
SNSSubscription              Script to be subscribed to an SNS Topic
SQSQueueProcessor            Script to be subscribed to an SQS Queue

現時点では8個のテンプレートがあります。
個々のテンプレートの詳細には触れませんがGitHubでソースを見れますので気になる人はそちらをご覧ください。

テンプレートから関数生成

本記事では一番基本的なBasicテンプレートからHelloPowerShellLambdaという名前の関数を生成してみます。
任意のディレクトリに移動しNew-AWSPowerShellLambdaコマンドレットから関数を生成することができます。

cd [任意のディレクトリ]
New-AWSPowerShellLambda -ScriptName HelloPowerShellLambda -Template Basic

HelloPowerShellLambdaフォルダとHelloPowerShellLambda.ps1が作成されていることがわかります。 このHelloPowerShellLambda.ps1スクリプトの中身はこんな感じです。

関数の修正

今回はこのテンプレートを以下の様に修正してみます。
入力をそのまま出力するだけの非常に簡単な関数です。

$message = $LambdaInput.Message
Write-Output ("Hello {0}!(on PowerShell Core {1})" -f $message, ($PSVersionTable.PSVersion.ToString()))

ここでPowerShellのLambda関数について基本的な仕様を説明しておくと、

  • 入力は$LambdaInput変数
    • 内部的には入力のJSONをConvertFrom-Jsonで変換して生成している
  • 出力はストリームに最後に出力されたオブジェクトがJSONの形で出力される
    • 内部的にはConvertTo-JsonでJSONとして出力している
  • モジュールを使いたい場合は#Requires文を記述する
    • 関数パッケージの生成時に#Requires文で指定したモジュールが同梱される
  • 関数の内部情報には$LambdaContext変数でアクセスできる

となっており、これだけ押さえておけば問題ないでしょう。

Lambda関数をデプロイしてみる

作成した関数をデプロイするにはPublish-AWSPowerShellLambdaコマンドレットを使います。
基本的には以下の引数を指定すればOKです。

  • ScriptPath : デプロイするスクリプト
  • Name : 関数名
  • Region : デプロイするリージョン
  • Profile : 使用するプロファイル
cd .\HelloPowerShellLambda
Publish-AWSPowerShellLambda -ScriptPath .\HelloPowerShellLambda.ps1 -Name HelloPowerShellLambda -Region ap-northeast-1 -Profile MyProfile

関数を新規作成する場合、不足している情報があれば都度プロンプトで入力を求められます。

今回はIAMRoleの入力が求められましたのでHelloPowerShellLambdaRoleという名前でAWSLambdaFullAccessポリシーをアタッチしておきます。
ちなみに予めIAMRoleを作成している場合は-IAMRoleArnパラメーターを使うことも可能です。

上図の様にエラーなく終了すればデプロイは完了です。
コンソールを確認するとちゃんと関数が登録されています。

以降の設定はコンソールから行い関数を実行可能にしてください。
ここからはPowerShellに限らないLambdaの一般的なはなしになるので本記事では割愛します。

Lambda関数をテストしてみる

本記事ではこの関数をテストするところまでやってみます。
次の単純なJSONSampleInputを定義してこの関数をテストしてみます。

{
  "Message": "Lambda"
}

結果は以下の様になり、ちゃんとHello Lambdaのメッセージが出力できました。

あとは必要に応じてLambdaの設定をして運用すれば良いでしょう。

補足

本記事で試すのはここまでにして最後にいくつか補足的なお話をします。

補足1. .NET Core SDKが必要な理由

最初に触れた.NET Core SDKが必要な理由についてですが、これはPowerShell Lambdaのアーキテクチャに理由があります。
PowerShell Lambdaは、ユーザーが記述するのはPowerShellのスクリプトですが、関数パッケージはC#のアセンブリを都度作成しており実体としてはC# Lambdaであるためアセンブリのコンパイルに.NET Core SDKが必要なります。

New-AWSPowerShellLambdaPackageコマンドレットで関数パッケージの生成を行えますので、実際にパッケージを生成し中身を確認してみます。

New-AWSPowerShellLambdaPackage -ScriptPath .\HelloPowerShellLambda.ps1 -OutputPackage .\Package.zip

これでスクリプトと同じフォルダにPackage.zipという名前のパッケージが生成されます。

このパッケージの中身を見てみるとスクリプト本体のHelloPowerShellLambda.ps1以外に多くのDllが同梱されていることがわかります。
この内、スクリプトと同名のHelloPowerShellLambda.dllが実際のLambda関数のエントリーポイントとなっており中身はこんな感じです。

# HelloPowerShellLambda.dll
using Amazon.Lambda.PowerShellHost;

namespace HelloPowerShellLambda.Bootstrap
{
    public class Bootstrap : PowerShellFunctionHost
    {
        public Bootstrap() : base("HelloPowerShellLambda.ps1")
        {
        }
    }
}

# PowerShellFunctionHostクラス
# https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.PowerShellHost/PowerShellFunctionHost.cs
# より一部抜粋
namespace Amazon.Lambda.PowerShellHost
{
    /// <summary>
    /// Base class for Lambda functions hosting PowerShell Core runtime
    /// </summary>
    public abstract class PowerShellFunctionHost
    {
        /// <summary>
        /// Creates an instances of the class. As part of creation it will initiate the PowerShell Core runtime and load any required PowerShell modules.
        /// </summary>
        protected PowerShellFunctionHost()
        {
            // This will only be true for local testing.
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
            {
                var state = InitialSessionState.CreateDefault();
                state.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted;
                this._ps = PowerShell.Create(state);                
            }
            else
            {
                this._ps = PowerShell.Create();                                
            }

            this.SetupStreamHandlers();
            this.LoadModules();

            // Can be true if there was an exception importing modules packaged with the function.
            if(this._lastException != null)
            {
                Console.WriteLine(this._constructorLoggingBuffer.ToString());
                throw this._lastException;
            }

            this.PowerShellFunctionName = Environment.GetEnvironmentVariable(POWERSHELL_FUNCTION_ENV);
            if(!string.IsNullOrEmpty(this.PowerShellFunctionName))
            {
                this._constructorLoggingBuffer.AppendLine($"Configured to call function {this.PowerShellFunctionName} from the PowerShell script.");
            }
        }

        /// <inheritdoc />
        /// <summary>
        /// Creates an instances of the class. As part of creation it will initiated the PowerShell Core runtime and load any required PowerShell modules.
        /// </summary>
        /// <param name="powerShellScriptFileName">The PowerShell script that will run as part of every Lambda invocation</param>
        protected PowerShellFunctionHost(string powerShellScriptFileName)
            : this()
        {
            this._powerShellScriptFileName = powerShellScriptFileName;
        }

        /// <summary>
        /// AWS Lambda function handler that will execute the PowerShell script with the PowerShell Core runtime initiated during the construction of the class.
        /// </summary>
        /// <param name="inputStream"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public Stream ExecuteFunction(Stream inputStream, ILambdaContext context)
        {
            this._lastException = null;

            if (this._runFirstTimeInitialization)
            {
                this._logger = context.Logger;

                if (this._constructorLoggingBuffer?.Length > 0)
                {
                    context.Logger.Log(this._constructorLoggingBuffer.ToString());
                    this._constructorLoggingBuffer = null;
                }

                this._runFirstTimeInitialization = false;
            }

            string inputString;
            using (var reader = new StreamReader(inputStream))
            {
                inputString = reader.ReadToEnd();
            }

            var result = this.BeginInvoke(inputString, context);
            this.WaitPowerShellExecution(result);

            if (this._lastException != null || this._ps.InvocationStateInfo.State == PSInvocationState.Failed)
            {
                var exception = this._exceptionManager.DetermineExceptionToThrow(this._lastException ?? this._ps.InvocationStateInfo.Reason);
                throw exception;
            }

            return new MemoryStream(Encoding.UTF8.GetBytes(this.GetExecutionOutput()));
        }
    }
}

ソースの細かい説明まではしませんが、PowerShellFunctionHostクラスが内部でPowerShellのオブジェクトを生成しコンストラクタで指定されたスクリプトを実行しているのがわかります。

補足2. Community Posts

GitHubのドキュメントにCommunity Postsとして海外勢のPowerShell Lambdaを試してみた系ブログが紹介されています。

こちらの内容も参考になりますのでぜひご覧ください。