こんにちは、エデュケーショナルサービス課の小倉です。
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