環境イメージ
Lambdaを2つ用意します。
1つはEC2インスタンスが停止しているかを確認するLambda。もう1つはインスタンスタイプを変更するLambdaになります。この2つのLambdaをStep Functionsで制御していきます。
停止が確認取れた場合にのみ、次のジョブを実行する形にします。
複雑な制御も出来ると思いますが、今回は出来るだけシンプルな構成で作っていきます。
環境構築・ハンズオン
Lambda
関数を適当に作ります。
Python3.13を使用していきます。

また、それぞれのLambdaに紐づいているIAMロールにEC2FullAccessというPolicyを紐づけます。
これを付けていないとLambdaからEC2へ操作が一切効かず、Lambda実行時にエラーを吐かれます。

sf-ec2-instance-stop-check
import boto3
from botocore.exceptions import ClientError
import json
# Python 3.13 ランタイム
# 必要なIAM権限: ec2:DescribeInstances
def lambda_handler(event, context):
ec2 = boto3.client('ec2')
# Step Functionsから {"instance-id": "i-xxxxxxxx"} という形式で渡される想定
target_id = event.get('instance-id')
if not target_id:
raise ValueError("Error: 'instance_id' is required in the input event.")
try:
# 特定のインスタンスIDを指定して情報を取得
response = ec2.describe_instances(InstanceIds=[target_id])
# 配列構造からステータスを取り出す
instance_data = response['Reservations'][0]['Instances'][0]
state_name = instance_data['State']['Name']
return {
"instance_id": target_id,
"status": state_name, # running, stopped, pending, stopping, etc.
"exists": True
}
except ClientError as e:
# エラーハンドリング(例: インスタンスが存在しない場合など)
print(f"AWS Error: {e}")
raise e # Step Functionsにエラーを通知するために再送出する、または適切なJSONを返す
except Exception as e:
print(f"Unexpected Error: {e}")
raise e
sf-ec2-instance-type-change
対象のEC2のインスタンスタイプがLambda実行時t3.microならt3.smallに変更し、t3.smallであればt3.microに変更するものになります。
import json
import boto3
from botocore.exceptions import ClientError
def lambda_handler(event, context):
# クライアント作成
client = boto3.client('ec2')
# Step Functionsから {"instance-id": "i-xxxxxxxx"} という形式で渡される想定
target_id = event.get('instance-id')
print(f"対象インスタンス: {target_id}")
try:
# 1. 現在のインスタンスタイプを取得
response = client.describe_instances(InstanceIds=[target_id])
# インスタンスが存在しない場合のエラーハンドリング
if not response['Reservations']:
return _response(404, f'{target_id}が見つかりません。')
current_type = response['Reservations'][0]['Instances'][0]['InstanceType']
print(f"現在のタイプ: {current_type}")
# 2. タイプの切り替え判定 (トグル動作)
if current_type == 't3.micro':
new_type = 't3.small'
elif current_type == 't3.small':
new_type = 't3.micro'
else:
msg = f'変更スキップ: 現在のタイプは {current_type} です。対象外のため変更しません。'
print(msg)
return _response(200, msg)
# 3. インスタンスタイプの変更実行
# (停止チェックはしていませんが、起動中の場合はここでAWSからエラーが返り例外処理へ飛びます)
client.modify_instance_attribute(
InstanceId=target_id,
InstanceType={
'Value': new_type
}
)
success_msg = f'成功: {current_type} から {new_type} に変更しました。'
print(success_msg)
return _response(200, success_msg)
except ClientError as e:
# AWS側のエラー(例: インスタンスが起動中で変更できない場合など)をキャッチ
error_code = e.response['Error']['Code']
error_msg = e.response['Error']['Message']
print(f"AWS Error: {error_code} - {error_msg}")
return _response(500, f"AWS処理エラー: {error_msg}")
except Exception as e:
# その他の予期せぬエラー
print(f"Error: {e}")
return _response(500, f"予期せぬエラー: {e}")
def _response(code, message):
"""レスポンス生成用ヘルパー関数"""
return {
'statusCode': code,
'body': json.dumps(message, ensure_ascii=False)
}
EC2
対象となるEC2を適当に作っておきます。
パブリックサブネットに一応作成。インスタンスタイプはt3.microで作成します。作成後は停止しておきます。

Step Functions
ステートマシンの作成は空白から作成・標準を指定します。
画面の左のアクションからLambda InvokeをD&Dします。
画面右の関数名にsf-ec2-instance-stop-checkを選択します。
またペイロードには以下のように入力します。
{
"instance-id": "i-002cd95c94f1a9c73"
}
引数には自動入力でLambdaやペイロードの情報が記載されていることを確認します。
{
"FunctionName": "arn:aws:lambda:ap-northeast-1:535002847634:function:sf-ec2-instance-stop-check:$LATEST",
"Payload": {
"instance-id": "i-002cd95c94f1a9c73"
}
}
同じような設定をもう片方のLambdaに設定しておきます。
そしてChoiceとfailedを画面左のフロータブからD&Dして以下のように配置します。
ChoiceをアクティブにしてRule#1のConditionに以下のような設定を入れます。
{% $states.input.status = "stopped" %}
sf-ec2-instance-stop-checkに戻って、エラー処理のErrorsのところにStatus:ALLを指定します。
これを指定することでLambdaが実行できなかった場合もFailに推移するようにしています。
画面右上の保存を押下します。

Step FunctionsからLambdaを呼び出すのに不足しているIAMロールを自動で作成してくれます。
確認ボタンを押下します。

Step FunctionsをJSON形式にしたものが以下になります。
{
"Comment": "A description of my state machine",
"StartAt": "Invoke-ec2-stop-check-lambda",
"States": {
"Invoke-ec2-stop-check-lambda": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Output": "{% $states.result.Payload %}",
"Arguments": {
"FunctionName": "arn:aws:lambda:ap-northeast-1:535002847634:function:sf-ec2-instance-stop-check:$LATEST",
"Payload": {
"instance-id": "i-002cd95c94f1a9c73"
}
},
"Retry": [
{
"ErrorEquals": [
"Lambda.ServiceException",
"Lambda.AWSLambdaException",
"Lambda.SdkClientException",
"Lambda.TooManyRequestsException"
],
"IntervalSeconds": 1,
"MaxAttempts": 3,
"BackoffRate": 2,
"JitterStrategy": "FULL"
}
],
"Next": "Choice",
"Catch": [
{
"ErrorEquals": [
"States.ALL"
],
"Next": "Fail"
}
]
},
"Choice": {
"Type": "Choice",
"Choices": [
{
"Next": "Invoke-ec2-instance-type-change-lambda",
"Condition": "{% $states.input.status = \"stopped\" %}"
}
],
"Default": "Fail"
},
"Invoke-ec2-instance-type-change-lambda": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Output": "{% $states.result.Payload %}",
"Arguments": {
"FunctionName": "arn:aws:lambda:ap-northeast-1:535002847634:function:sf-ec2-instance-type-change:$LATEST",
"Payload": {
"instance-id": "i-002cd95c94f1a9c73"
}
},
"Retry": [
{
"ErrorEquals": [
"Lambda.ServiceException",
"Lambda.AWSLambdaException",
"Lambda.SdkClientException",
"Lambda.TooManyRequestsException"
],
"IntervalSeconds": 1,
"MaxAttempts": 3,
"BackoffRate": 2,
"JitterStrategy": "FULL"
}
],
"End": true
},
"Fail": {
"Type": "Fail"
}
},
"QueryLanguage": "JSONata"
}
動作確認
このような画面が表示されます。特に設定を入れずに実行を開始を押下します。

実行が全て成功するとそれぞれのLambdanの背景が緑色に変わります。

EC2を見てみるとインスタンスタイプが変わっていることがわかります。
上手く動いていそうですね。

失敗しましたね。
ここではインスタンスタイプがstopped以外であるために、Failに処理が分岐したという事になります。








