Step Functions Workflow Studio を利用して、EC2インスタンス開始30分後にEC2を停止するワークフローを組んでみた

GUIからStep Functionsのワークフローを作成し、30分しかEC2を起動させてもらえない厳しい環境を作ります。Step Functions, EventBridge, Lambdaを同時に学べます。
2021.07.07

以前チュートリアルでStep Functionsにはじめて触れたのですが、チュートリアルだとワークフロー作成の感覚がつかめず終わりました。前回の反省を活かし適当なお題を作って、Step Functionsを利用してお題を解いてみます。

肝心なワークフロー作成部分はGUIから作成できるようになりました。これも試してみたかったのでGUIだけでワークフローを作成してみます。

ワークフローのお題

「停止中のEC2インスタンスを開始(起動)すると、30分経過後にインスタンスを停止する」をStep Functionsを利用してやってみます。絶対EC2は30分しか使わせないマンを作ります。Step Functionsの担当は30分待機してからインスタンスを停止するLambdaを呼ぶの部分です。

お題を整理

Step Functionsのワークフローを開始するためのトリガーが必要です。EC2インスタンスが開始するとCloudTrailにログが記録されます。CloudTrailのログをトリガーとするならば、特定のログが記録されるのを常時監視しているなにかが必要です。そこはEventBridgeのイベントルールで実現できます。

EventBridgeのイベントルールで特定のログを検知した後、Step Functionsのステートマシンを開始する必要があります。これはEventBridgeのイベントルールからターゲットという設定があり、Step Functionsのステートマシンを呼び出せます。そして、EventBridgeのイベント内容(何時何分に誰がEC2を開始したといった内容のJSON)をStep Functionsへ引き渡すこともできます、便利。

Step Functionsのステートマシンでは、30分間待機してからLambdaを実行してあげればお題を達成できそうです。LambdaはどのEC2インスタンスを停止してよいかわからないので、EventBridgeから受け取ったイベントの内容をStep Functionsのステートマシン経由でLambdaへ引き渡します。Lambdaはイベント内容からインスタンスIDを特定してインスタンスを停止させます。

作業工程

Lambda

EventBridgeからのインプット(JSON)からEC2のインスタンスIDを特定し、そのインスタンスIDに対して停止を行う処理をします。 LambdaのIAMロールにはEC2を停止する権限が必要です。

package main

import (
	"fmt"
	"log"
	"time"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/ec2"
)

type EC2Events struct {
	Detail     Detail        `json:"detail"`
}
type Items struct {
	InstanceID string `json:"instanceId"`
}
type InstancesSet struct {
	Items []Items `json:"items"`
}
type RequestParameters struct {
	InstancesSet InstancesSet `json:"instancesSet"`
}
type Detail struct {
	EventVersion       string            `json:"eventVersion"`
	EventTime          time.Time         `json:"eventTime"`
	EventSource        string            `json:"eventSource"`
	EventName          string            `json:"eventName"`
	AwsRegion          string            `json:"awsRegion"`
	SourceIPAddress    string            `json:"sourceIPAddress"`
	UserAgent          string            `json:"userAgent"`
	RequestParameters  RequestParameters `json:"requestParameters"`
	RequestID          string            `json:"requestID"`
	EventID            string            `json:"eventID"`
	ReadOnly           bool              `json:"readOnly"`
	EventType          string            `json:"eventType"`
	ManagementEvent    bool              `json:"managementEvent"`
	RecipientAccountID string            `json:"recipientAccountId"`
	EventCategory      string            `json:"eventCategory"`
}

const (
	REGION = "ap-northeast-1"
)

type Response struct {
	Payload string `json:"payload"`
}

func handler(event EC2Events) (Response, error) {
	instanceID := event.Detail.RequestParameters.InstancesSet.Items[0].InstanceID

	sess := session.Must(session.NewSession())
	svc := ec2.New(
		sess,
		aws.NewConfig().WithRegion(REGION))

	res, err := stopInstance(svc, instanceID)
	if err != nil {
		return Response{Payload: "error"}, err
	}
	fmt.Println(res)

	return Response{Payload: res}, nil
}

func main() {
	lambda.Start(handler)
}

func stopInstance(svc *ec2.EC2, id string) (string, error) {
	input := &ec2.StopInstancesInput{
		InstanceIds: []*string{
			aws.String(id),
		},
	}

	result, err := svc.StopInstances(input)
	if err != nil {
		log.Println(err)
	}
	log.Println(result)
	currentStateName := *result.StoppingInstances[0].CurrentState.Name
	preciousStateName := *result.StoppingInstances[0].PreviousState.Name
	stopResult := ""
	if currentStateName == "stopping" && preciousStateName == "running" {
		stopResult = "Succeed"
	} else {
		stopResult = "failed"
	}
	return stopResult, err
}

Step Functions

30分待機してからLambdaを呼び出すワークフローをGUIから作成します。

Step Functions Workflow Studioを使います。タイプは標準を利用します。

左ペインからドラック&ドロップでワークフローを作っていきます。

Wait Stateを配置しました。

1800秒(30分)待機させます。これで一定時間待機してからLambdaを起動できます。

次にLambda: Invokeを配置しました。

事前に作成済みのLambdaを設定します。GUIだと余り知識なくてもワークフロー完成しました。

次へをクリック。

Step Functionsのワークフローを記述するASL(Amazon States Language)が自動生成されました。

ステートマシン名、ログ設定など設定します。

Step FunctionsからLambdaを呼び出すのに必要なIAMポリシー、IAMロールを作成してくれます。Lambdaの呼び出しに必要なポリシーを作成してくれました。

30分待機してからLambdaを実行するステートマシンの作成完了です。

EventBridge

EC2インスタンスの作成、開始を検知するとStep Functionsを実行するとイベントルールを作成します。

ルールを作成をクリック。

パターンの定義が悩みどころです。特定のオペレーションで停止中のEC2が開始(起動)したときを指定したいです。さて特定のオペレーションに何を入れたいいのか?

素直にCloudTrailのログからEC2開始のログを確認しました。開始のイベント名はStartInstancesでした。ちなみに新規作成のイベント名はRunInstancesでした。以下の記事もご参考に。

以下の様にイベント名を入力します。入力するとイベントパターンの欄にeventNameとして追加されます。

イベントルールで検知するとStep Functionsのワークフローを実行したいため、ターゲットは以下の様に設定します。

イベントルールの作成完了です。

ステートマシン実行テスト

停止状態のEC2を開始にしました。

ステートマシンが実行中になっています。EventBridgeのイベントルールが検知し、Step Functionsのステートマシンの開始に成功しました。

グラフインスペクターで確認するとWait Stateが処理中です。30分待機に設定していたためしばらく待ちます。テストすることを考えたら初回は待機時間を短く設定しておくべきでした。。。

放置していたら終わっていました。

実行イベント履歴を確認します。開始直後にWaitで30分待機がはじまり、タイムスタンプを確認すると30分待ってから後続の処理が流れていることを確認できます。

EC2停止Lambda実行によりインスタンスが停止しています。これで30分しか使わせないEC2環境の完成です。

ステートマシンに条件分岐を足してみる

Lambdaの実行結果を元に成功したか、失敗したかをChoice stateで条件分岐させます。とくに意味はないのですけど条件分岐ってどうやるのか気になったのでやってみます。

条件分岐の条件に使える値が欲しいです。前回の実行結果からLambdaの結果(ステップ出力)を確認します。どのような結果が返っていたのでしょうか。

以下のJSONが返ってきています。ステップ出力の値はLambdaでEC2のインスタンス停止が成功したらSucceedを返すように実装されていたものです。LambdaでreturnしたJSONの文字列がそのままステップ出力の結果となりました。

{
  "payload": "Succeed"
}

Chois stateからSuccessに流れる左側のルートRule #1を編集します。

ステップ出力のJSONをパースする記述があり、$.payloadSucceedの値を取得できました。Succeedの文字列にマッチしたらRule #1ルートへ進むように設定します。

$.がわからなく苦戦しました。試行錯誤の末やっと値を取得にたどり着きました。Step Functionsについて調べないとWorkflow Studioだから簡単というわけではありませんでした。下記のブログポストを参考にしました。

デフォルトはFailのルートへ進むため、成功だけ設定しました。ついでにWaitの待機時間を30分から10秒に変更しました。

ASLは自動生成され記述は以下のようになっています。

{
  "Comment": "This is your state machine",
  "StartAt": "Wait",
  "States": {
    "Wait": {
      "Type": "Wait",
      "Seconds": 10,
      "Comment": "10s",
      "Next": "Lambda Invoke"
    },
    "Lambda Invoke": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "OutputPath": "$.Payload",
      "Parameters": {
        "Payload.$": "$",
        "FunctionName": "arn:aws:lambda:ap-northeast-1:123456789012:function:stopEC2instance:$LATEST"
      },
      "Retry": [
        {
          "ErrorEquals": [
            "Lambda.ServiceException",
            "Lambda.AWSLambdaException",
            "Lambda.SdkClientException"
          ],
          "IntervalSeconds": 2,
          "MaxAttempts": 6,
          "BackoffRate": 2
        }
      ],
      "Next": "Choice"
    },
    "Choice": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.payload",
          "StringMatches": "Succeed",
          "Next": "Success"
        }
      ],
      "Default": "Fail"
    },
    "Success": {
      "Type": "Succeed"
    },
    "Fail": {
      "Type": "Fail"
    }
  }
}

ステートマシン実行テスト2回目

またEC2を停止状態から開始にします。

Successに進みました。EC2も開始まもなくして停止させられました。

Failの条件を簡単に満たすためにステートマシンを手動実行します。

EventBridgeから渡されるJSONをそのまま貼り付けます。このインスタンスIDのEC2は現在停止状態です。停止状態でLambdaから停止をかけようとするためfailedを返すよう実装しています。

「EventBridgeから渡されるJSONをそのまま」は前回実行したステートマシンの履歴から、Wait stateのステップ入力からコピーしてきています。

手動実行するとFailへ流れました。Choice stateでステップ出力に応じた条件分岐も試せました。

おわりに

共有のAWSアカウントでEC2は30分しか使わせませんワークフローを勝手に仕込まれるとえらい迷惑です。そんなことをしたかったわけではなく、標準タイプのワークフローの作成とともに確認したいことが2点ありました。Wait Stateの最大待機時間と、ステートマシンの最大実行時間です。

ステートマシンの最大実行時間は1年。Wait Stateも1年待機できるが最大実行時間の1年の制限がある。実質待機できるのは他の処理時間 - 1年となる。

標準タイプのワークフローを利用する分には普段から意識しておく必要がないくらいに実行時間、待機時間に余裕がありました。

参考