こんにちは、エデュケーショナルサービス課の小倉です。
2024/11にVPC内のLambdaやEC2からEventBridge Schedulerを変更したかったのですが、当時はEventBridge SchedulerのVPCエンドポイントがなかったため、Step Functionsなど別の方法にしないといけませんでした。その後、2025/3にPrivateLink(VPCエンドポイント)がサポートされ、VPC内からEventBridge Schedulerにアクセスできるようになりました。
EventBridgeでVPC Lambdaを起動し、EC2を停止しようとしたとします。疲れていたのか、このアップデートを見たときに、以下のような通信ができるようになったのかと思ったのですが、そうではなかったです。私はもともとネットワークエンジニアなので、通信経路がとても気になります。
なにもせずにLambdaを使用するのであれば、VPCを経由せずにVPC外の通信のみで完結します。
もし、VPC Lambdaを定期実行するにはどうすればよいのでしょうか。
EventBridge Schedulerを使うのですが、想定の通信経路はこのようになるはずです。
念のため、VPCフローログを有効にして、VPC LambdaのENIの通信を確認したところ、EC2のVPCエンドポイントに通信していることが確認できています。ただ、なぜか最初に通信しているのがEC2 VPCエンドポイントからVPC Lambdaあての通信になっていました。VPC外の通信は確認することができないので、どのような通信をしているのかが不明です。とても気になります。
- VPC Lambda ENI IPアドレス:10.0.128.81
- EC2 VPCエンドポイント IPアドレス:10.0.131.82
2025-06-04T20:35:34.000+09:00 2 xxxxxxxxxxxx eni-xxxxxxxxxxxxxxxxx 10.0.131.82 10.0.128.81 443 8138 6 22 8667 1749036934 1749036994 ACCEPT OK 2025-06-04T20:36:25.000+09:00 2 xxxxxxxxxxxx eni-xxxxxxxxxxxxxxxxx 10.0.128.81 10.0.131.82 8138 443 6 18 5536 1749036985 1749037045 ACCEPT OK
まとめ
VPC Lambdaを使用している場合でも、EventBridge Schedulerを使用して、定期実行をすることができます。VPC内とVPC外のサービスを連携させるときは通信経路を確保するようにしましょう。
おまけ
EC2を停止するコードを生成AIに作ってもらいました。以下が生成されたコードですが、こんなのが簡単にできてしまうとはすごいですね。
import boto3
import json
import logging
from botocore.exceptions import ClientError, BotoCoreError
# ログ設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
"""
実行中のEC2インスタンスを停止するLambda関数
"""
try:
# EC2クライアントの初期化
client = boto3.client('ec2')
# 実行中のインスタンス情報を取得
logger.info("実行中のEC2インスタンスを検索中...")
response_describe = client.describe_instance_status(
Filters=[
{
'Name': 'instance-state-name',
'Values': ['running']
}
]
)
instance_statuses = response_describe['InstanceStatuses']
if not instance_statuses:
logger.info("停止対象のEC2インスタンスが見つかりませんでした")
return {
'statusCode': 200,
'body': json.dumps({
'message': '停止対象のインスタンスはありません',
'stopped_instances': []
})
}
stopped_instances = []
failed_instances = []
# 各インスタンスを停止
for instance_status in instance_statuses:
instance_id = instance_status['InstanceId']
try:
logger.info(f"インスタンス {instance_id} を停止中...")
response_stop = client.stop_instances(InstanceIds=[instance_id])
# 停止処理の結果を確認
stopping_instances = response_stop.get('StoppingInstances', [])
if stopping_instances:
current_state = stopping_instances[0]['CurrentState']['Name']
previous_state = stopping_instances[0]['PreviousState']['Name']
logger.info(f"インスタンス {instance_id}: {previous_state} -> {current_state}")
stopped_instances.append({
'instance_id': instance_id,
'previous_state': previous_state,
'current_state': current_state
})
except ClientError as e:
error_code = e.response['Error']['Code']
error_message = e.response['Error']['Message']
logger.error(f"インスタンス {instance_id} の停止に失敗: {error_code} - {error_message}")
failed_instances.append({
'instance_id': instance_id,
'error': f"{error_code}: {error_message}"
})
except Exception as e:
logger.error(f"インスタンス {instance_id} の停止中に予期しないエラー: {str(e)}")
failed_instances.append({
'instance_id': instance_id,
'error': f"予期しないエラー: {str(e)}"
})
# 結果をログ出力
logger.info(f"停止成功: {len(stopped_instances)}件, 失敗: {len(failed_instances)}件")
# レスポンスを返す
return {
'statusCode': 200,
'body': json.dumps({
'message': 'EC2インスタンス停止処理が完了しました',
'stopped_instances': stopped_instances,
'failed_instances': failed_instances,
'summary': {
'success_count': len(stopped_instances),
'failed_count': len(failed_instances)
}
}, ensure_ascii=False)
}
except ClientError as e:
error_message = f"AWS API エラー: {e.response['Error']['Code']} - {e.response['Error']['Message']}"
logger.error(error_message)
return {
'statusCode': 500,
'body': json.dumps({
'error': error_message
})
}
except Exception as e:
error_message = f"予期しないエラーが発生しました: {str(e)}"
logger.error(error_message)
return {
'statusCode': 500,
'body': json.dumps({
'error': error_message
})
}
小倉 大(記事一覧)
アプリケーションサービス部エデュケーショナルサービス課 札幌在住
AWSトレーニングの講師をしています。
最近は7歳の息子と遊ぶのが楽しいです!
Twitter: @MasaruOgura