【登壇】Interact 2019で紹介したデモ(PowerShell Lambda)の解説 #msinteract19 #IN06

しばたです。
先週登壇したInteract 2019で紹介したデモについて、環境構築の部分をセッション中では説明できなかったので本記事で補足します。

【登壇】Interact 2019でPowerShell CoreとPowerShell 7についてお話してきました #msinteract19 #IN06

デモの内容について

デモの内容については上記リンク先の記事にもありますが、クラウド管理の例としてPowerShell Lambdaを使い特定のタグ(AutoShutdown = true)が付いたEC2インスタンスを停止させる関数を紹介しました。
内容としては割とあるあるな奴です。

PowerShell Lambdaの基本構成

最初にAWS Lambdaの基本として以下のリソースが必要となります。

  • Lambda関数本体
  • 関数を起動するためのトリガー
  • 関数を実行する為の権限(IAMロール)

大抵の場合上記リソースはすべてCloudFormationで作成することができます。

私も最初は必要なリソースをすべてCloudFormationで作成しようとしたのですが、関数本体のリソースであるAWS::Lambda::Function リソースCodeプロパティでソースコードを直接記述可能(ZipFIleを選択可能)なのがNode.jsとPythonであり、その他の言語で事前にS3に関数のコードをアップロードしておく必要があるため、その手間を考えるとCloudFormationを使わず、AWSLambdaPSCoreモジュールPublish-AWSPowerShellLambdaコマンドレットを使った方が良いだろうと判断しました。
このため紹介したデモでは事前に最低限必要なリソースのみCloudFormationで作成する手順としています。

ちなみになのですが、AWS CLIのaws cloudformation packageコマンドを使うとローカルファイルをS3にアップロードしつつスタックを作ることが可能なのですが、これに等価なPowerShellコマンドレットは提供されていませんでした...

デモ環境の作成

ここからデモ環境の作成手順を説明していきます。

1. CloudFormationの実行

にあるCloudFormationテンプレートをPowerShellから実行して関数の実行に必要な

  • IAMロール
  • CloudWatchイベント

を作成します。

Import-Module AWSPowerShell.NetCore
Set-DefaultAWSRegion -Region ap-northeast-1
Set-AWSCredential -ProfileName "set your profile"

# 事前準備
# CFnからあらかじめ必要なリソースを作成
$params = @{
    StackName = "interact2019-lambda-demo-base";
    TemplateBody = (Get-Content -LiteralPath '.\lambda-base-cfn.yaml' -Raw);
    Parameter = &{
        $p1 = New-Object Amazon.CloudFormation.Model.Parameter 
        $p1.ParameterKey = "BaseName" 
        $p1.ParameterValue = "interact2019-lambda-demo"
        return @($p1)
    };
    Capability = "CAPABILITY_NAMED_IAM"
}
New-CFNStack @params

New-CFNStackコマンドレットで新しいスタックを作成することができます。
-TemplateBodyパラメータにテンプレートの内容を設定する必要があるのでGet-Contentでファイルから内容を読み込んでいます。
テンプレートに引き渡すパラメーターは-Parameterで指定するのですが、これがAmazon.CloudFormation.Model.Parameter型のオブジェクトとなっており少し面倒です。
今回はIAMロールを作成するので-CapabilityパラメーターにCAPABILITY_NAMED_IAMをセットしておく必要もあります。

コマンドレットを実行するとスタックIDを返しますので後は結果を確認すればOKです。

2. 関数本体のデプロイ

以前にも説明したとおり、Lambda関数をデプロイするにはPublish-AWSPowerShellLambdaコマンドレットを使います。

Import-Module AWSLambdaPSCore
$params = @{
    Name = "interact2019-lambda-demo";
    ScriptPath = ".\lambda\Function.ps1"
    IAMRoleArn = ("arn:aws:iam::{0}:role/interact2019-lambda-demo-role" -f ((Get-STSCallerIdentity).Account))
}
Publish-AWSPowerShellLambda @params

事前準備などについては以前の記事をご覧ください。
今回は前項の手順でIAMロールを作成済みですので-IAMRoleArnパラメーターにIAMロールのARNを指定してあります。
Get-STSCallerIdentityコマンドレットで呼び出し元のアカウントIDなどの情報を取得しています。

するとスクリプトを含んだアセンブリのコンパイルとデプロイを行ってくれます。

この状態でLambda関数の定義を確認すると下図の様になります。

関数本体とIAMロールは表示されましたが、トリガー(今回はCloudWatchイベント)がまだ関数と紐づいていません。

3. 関数にトリガーを紐づける

そこで続けてAdd-LMPermissionコマンドレットを使い最初に作成したCloudWatchイベントからLambda関数を呼び出すための許可(紐づけ)をしてやります。

# CloudWatch events のトリガーを許可
$params = @{
    FunctionName = "interact2019-lambda-demo";
    Action = "lambda:InvokeFunction";
    Principal = "events.amazonaws.com";
    SourceArn = ("arn:aws:events:ap-northeast-1:{0}:rule/interact2019-lambda-demo-event" -f ((Get-STSCallerIdentity).Account));
    StatementId = "interact2019-lambda-demo-event";
}
Add-LMPermission @params

トリガーの種類によって使うパラメーターは若干変わりますがCloudWatchイベントの場合はこんな感じです。

実行すると結果のJSONが返されます。
ここはあまりPowerShellらしくない感じです。

これで関数とトリガーが紐づきましたが、まだトリガー(CloudWatchイベント)側に実行する関数の指定が無いのでエラーがでています。

こちらはCloudWatchイベントの設定ですので、Write-CWETargetコマンドレットを使いターゲット指定をしてやります。
-TargetパラメーターはAmazon.CloudWatchEvents.Model.Target型のオブジェクトを指定してやる必要があり、関数のARNとリビジョンIDが必要になるのでGet-LMFunctionConfigurationを使い取得しています。

# CloudWatch eventsのターゲットにLambda関数を追加
$params = @{
    Rule = "interact2019-lambda-demo-event";
    Target = &{
        $func = Get-LMFunctionConfiguration -FunctionName "interact2019-lambda-demo"
        $target = New-object Amazon.CloudWatchEvents.Model.Target
        $target.Arn = $func.FunctionArn
        $target.Id = $func.RevisionId
        return $target
    };
}
Write-CWETarget @params

これですべての作業が完了しトリガー定義のエラーも解消されています。

関数本体について

最後に関数本体のコードについて簡単に解説します。

Lambda関数内でのモジュールの利用

Lambda関数内でPowerShellモジュールを利用する場合は#Requires -Modules文を記述します。

#Requires -Modules @{ModuleName='AWSPowerShell.NetCore';ModuleVersion='3.3.522.0'}

この記述によりデプロイ時に該当のモジュールを同梱し、関数実行前にロードする様になります。
今回はEC2インスタンスを停止する関数ですのでAWSPowerShell.NetCoreモジュールを使っています。

該当EC2インスタンスの取得

Get-EC2Instance-Filterパラメーターを指定することでAutoShutdownタグの値がtrueとなるEC2インスタンスを取得しています。

$instacnes = (Get-EC2Instance -Filter @{Name = 'tag:AutoShutdown'; Value = 'true' }).Instances
if ($null -eq $instacnes -or $instacnes.Count -eq 0) {
    Write-Host 'No instance found.'
    return 0
}

EC2インスタンスのシャットダウン

ループ内でGet-EC2InstanceStatusを使いインスタンスの状態を取得、状態がrunningであればStop-EC2Instanceを使いインスタンスを停止しています。

# ループしてシャットダウン
foreach ($i in $instacnes) {
    $status = Get-EC2InstanceStatus -InstanceId $i.InstanceId
    if ($status.InstanceState.Name -eq 'running') {
        Write-Host ("Attempt to stop EC2 instance {0}." -f ($i.InstanceId))
        Stop-EC2Instance -InstanceId $i.InstanceId
    }
    else {
        Write-Host ("EC2 instance {0} is not running. Skip to shutdown." -f ($i.InstanceId))
    }
}

余談ですがStop-EC2Instance-Terminateパラメーターを使用するとインスタンスを終了(削除)することができます。