VPC Lambda を定期実行することはできるのか?

記事タイトルとURLをコピーする

こんにちは、エデュケーショナルサービス課の小倉です。

2024/11にVPC内のLambdaやEC2からEventBridge Schedulerを変更したかったのですが、当時はEventBridge SchedulerのVPCエンドポイントがなかったため、Step Functionsなど別の方法にしないといけませんでした。その後、2025/3にPrivateLink(VPCエンドポイント)がサポートされ、VPC内からEventBridge Schedulerにアクセスできるようになりました。

aws.amazon.com

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