0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🤖AWS自動化:LambdaとEventBridgeでEC2・RDS・Aurora Serverless v2の起動停止とAuto Scalingのキャパシティ変更を実現する方法

Last updated at Posted at 2025-10-08

💡 はじめに

AWSの開発・ステージング環境では、EC2やRDSなどのインスタンス稼働コストの最適化が課題となることが多くあります。特に、夜間や週末といった利用されない時間帯にインスタンスを起動し続けることで、不要なコストが発生してしまいます。

このような 業務時間外の停止が許容される環境や、スケジュールに応じて稼働・停止を切り替えられるワークロード においては、インスタンスのスケールダウンや停止を自動化することで、大きなコスト削減が期待できます。

多くの方は手動でインスタンスの停止・起動を行っているかもしれませんが、手作業はエンジニアの工数を圧迫し、停止忘れや起動忘れといったヒューマンエラーを招く可能性があります。結果として、無駄なコストの増加や業務への悪影響が生じます。

この課題を解決するには、インスタンスのライフサイクルを自動化することが効果的なアプローチと考えられます。

AWSでは、EC2やRDSインスタンスを自動的に起動・停止させるための方法がいくつか提供されています。たとえば、AWS Systems Manager や Step Functions、EventBridge を活用することで、スケジュールに基づいた自動起動・停止の仕組みを構築するなど。

しかし、これらの構成には、VPCエンドポイントやNATゲートウェイの準備が必要になる場合もあり、設定が複雑になりがちです。さらに、インスタンスIDが変更されるたびに設定の見直しが必要になるなど、運用上の手間が発生します。

また、Auto Scaling Group(ASG)を利用した構成では、インスタンスを直接「停止」することができないため、キャパシティ(最小・最大・希望数)を0に変更してスケールインさせることが実質的な「停止」となり、再度キャパシティを戻すことで「起動」を実現します。こうしたASGの制御も、スケジュールベースで自動化することで、運用負荷の軽減と安定した稼働が可能となります。

このような課題に対して、私はCloudFormationを用いてLambdaとEventBridgeを組み合わせる方法が、シンプルで柔軟なアプローチだと考えています。Lambda関数内でインスタンスのタグを参照することで、対象を動的に特定でき、タグを変更するだけで制御対象を柔軟に切り替えることが可能です。インスタンスIDに依存しないため、変更が発生しても再設定の必要がなく、運用負荷の軽減にもつながります。

本記事では、CloudFormation、Lambda、EventBridgeを活用し、タグを利用したインスタンスの停止・起動・キャパシティー変更を自動化する仕組みを構築する方法を解説します。

🛠️ 構築するリソース

  1. Lambda関数の作成:
  • EC2、RDS、ASGの状態を取得し、特定のタグを持つリソースに対して起動・停止・キャパシティー変更(スケールアップ/ダウン)を実行します。
  • Pythonで実装されており、Boto3を使用して各サービスを操作します。
  1. EventBridgeルールの作成:

    EventBridgeで、特定の時間(例えば毎日午後10時)にLambdaをトリガーするスケジュール(Cron式で指定)を設定します。
  • AWS EventBridge Rule (Start): インスタンスを起動・スケールアップするためのスケジュールルール。

  • AWS EventBridge Rule (Stop): インスタンスを停止・スケールダウンするためのスケジュールルール。

  1. IAMロールの設定:

    Lambdaに必要な最小限の権限(EC2/RDS/ASGの操作、CloudWatch Logsへの書き込み)を付与します。

  2. CloudWatch Logs:

    Lambdaのログを取得するためCloudWatchログを作成します。

📦 CloudFormationスタックテンプレートの共有

まずは、今回構築する仕組みのベースとなる CloudFormation テンプレート を共有します。

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation Stack for Lambda to Turn EC2 Instances, RDS Instances, and Aurora Clusters v2 On/Off, and Scale Auto Scaling Groups on a Schedule

Parameters:

  InstancesStartTime:
    Type: String
    #Default: "cron(0 0 ? * MON-FRI *)" # 9 AM JST / 0 AM UTC Only Weekdays
    Default: "cron(0 0 ? * MON *)" # 9 AM JST / 0 AM UTC Only on Monday
    Description: "Start Time in Cron expression"

  InstancesStopTime:
    Type: String
    Default: cron(0 13 ? * FRI *)  # 22:00 PM JST only on Friday"
    #Default: cron(0 13 ? * MON-FRI *)  # 22:00 PM JST From Monday Friday"
    Description: "Stop Time in Cron expression"

  StartController:
    Type: String
    Description: Enable or disable the automatic start and scaling up of resources (e.g., EC2 instances and ASGs)
    Default: ENABLED
    AllowedValues:
      - ENABLED
      - DISABLED
  
  StopController:
    Type: String
    Description: Enable or disable the automatic stop and scaling down of resources (e.g., EC2 instances and ASGs)
    Default: ENABLED
    AllowedValues:
      - ENABLED
      - DISABLED


  PythonRuntime:
    Type: String
    Default: python3.13
    Description: Python Runtime Assigned for this Function

Resources:
  # CloudWatch Log Group for Lambda logs
  InstancesAutoTriggerLambdaLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub "/aws/lambda/instances-auto-trigger-lambda"  # Use the parameter for dynamic log group name
      RetentionInDays: 7  # Adjust retention period as needed (default: 7 days)

  # IAM Role for Lambda function with least privilege permissions
  InstancesAutoTriggerLambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "instances-auto-trigger-lambda-iamr"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: !Sub "instances-auto-trigger-lambda-iamp"
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              # Allow Lambda function to describe EC2 instances and RDS instances
              - Effect: Allow
                Action:
                  - ec2:DescribeInstances
                  - autoscaling:DescribeAutoScalingGroups
                  - rds:DescribeDBInstances
                  - rds:ListTagsForResource
                  - rds:DescribeDBClusters
                Resource: '*'
              - Effect: Allow
                Action:
                  - ec2:StopInstances
                  - ec2:StartInstances
                  - rds:StopDBInstance
                  - rds:StartDBInstance
                  - rds:StartDBCluster
                  - rds:StopDBCluster
                Resource: '*'
                Condition:
                  StringEquals:
                    aws:ResourceTag/InstancesAutoTrigger: "true"
              - Effect: Allow
                Action:
                  - autoscaling:UpdateAutoScalingGroup
                Resource: '*'
              # Allow Lambda to create logs for its execution (CloudWatch Logs permissions)
              - Effect: Allow
                Action:
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: "*"

  # Lambda Function
  InstancesAutoTriggerLambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Architectures: ["x86_64"]
      Handler: "index.lambda_handler"
      Runtime: !Ref PythonRuntime
      MemorySize: 128
      EphemeralStorage:
        Size: 512
      Timeout: 300
      Role: !GetAtt InstancesAutoTriggerLambdaExecutionRole.Arn
      FunctionName: !Sub "instances-auto-trigger-lambda"
      Environment:
        Variables:
          Instances_TAG_KEY: InstancesAutoTrigger
          Instances_TAG_VALUE: true
      Code:
        ZipFile: |
          import boto3
          import json
          import os
          import logging
          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          ec2 = boto3.client('ec2')
          rds = boto3.client('rds')
          asg = boto3.client('autoscaling')
          tag_key = os.environ.get('Instances_TAG_KEY', 'InstancesAutoTrigger')
          tag_value = os.environ.get('Instances_TAG_VALUE', 'true')

          def convert_state(state):
              return {
                  'running': '起動中',
                  'stopped': '停止中',
                  'stopping': '一時停止',
                  'shutting-down': '停止中',
                  'terminating': '停止中',
                  'available': '起動中',
                  'starting': '起動中',
                  'backing-up': 'バックアップ中',
                  'deleting': '削除中',
                  'deleted': '削除済み'
              }.get(state, f'不明な状態 ({state})')
          
          def log_with_header(header, lines):
              logger.info(header)
              if lines:
                  for line in lines:
                      logger.info(line)
              else:
                  logger.info("[]")
              logger.info("")
          
          def describe_ec2_instances():
              instances = []
              try:
                  res = ec2.describe_instances()
                  for r in res['Reservations']:
                      for inst in r['Instances']:
                          instances.append({
                              'InstanceId': inst['InstanceId'],
                              'State': inst['State']['Name'],
                              'Tags': {tag['Key']: tag['Value'] for tag in inst.get('Tags', [])}
                          })
              except Exception as e:
                  logger.error(f"EC2取得エラー: {e}")
              return instances
          
          def describe_rds_instances():
              instances = []
              try:
                  res = rds.describe_db_instances()
                  for db in res['DBInstances']:
                      tags = rds.list_tags_for_resource(ResourceName=db['DBInstanceArn'])['TagList']
                      instances.append({
                          'DBInstanceIdentifier': db['DBInstanceIdentifier'],
                          'DBInstanceStatus': db['DBInstanceStatus'],
                          'Tags': {tag['Key']: tag['Value'] for tag in tags}
                      })
              except Exception as e:
                  logger.error(f"RDS取得エラー: {e}")
              return instances
          
          def describe_aurora_clusters_v2():
              clusters = []
              try:
                  res = rds.describe_db_clusters()
                  for cluster in res['DBClusters']:
                      tags = rds.list_tags_for_resource(ResourceName=cluster['DBClusterArn'])['TagList']
                      clusters.append({
                          'DBClusterIdentifier': cluster['DBClusterIdentifier'],
                          # Aurora cluster status is top-level in the response
                          'Status': cluster['Status'], 
                          'Tags': {tag['Key']: tag['Value'] for tag in tags}
                      })
              except Exception as e:
                  logger.error(f"Aurora Cluster v2取得エラー: {e}")
              return clusters
         
          def describe_asg_groups():
              try:
                  return asg.describe_auto_scaling_groups()['AutoScalingGroups']
              except Exception as e:
                  logger.error(f"ASG取得エラー: {e}")
                  return []
          
          def process_ec2_instances(instances, action):
              running, stopped, others = [], [], []
              matched_ids, started, succeeded = [], [], []
              for inst in instances:
                  state = inst['State']
                  line = f"- EC2: {inst['InstanceId']} 状態: {convert_state(state)}"
                  if state == 'running':
                      running.append(line)
                  elif state == 'stopped':
                      stopped.append(line)
                  else:
                      others.append(line)
                  if inst['Tags'].get(tag_key) == tag_value:
                      if (action == 'stop' and state == 'running') or (action == 'start' and state == 'stopped'):
                          matched_ids.append(inst['InstanceId'])
              log_with_header("起動中EC2インスタンス:", running)
              log_with_header("停止中EC2インスタンス:", stopped)
              log_with_header("その他のステータスにあるEC2インスタンス:", others)
              logger.info(f"タグが一致する{('停止' if action == 'stop' else '起動')}対象EC2インスタンス: {matched_ids}")
              for instance_id in matched_ids:
                  try:
                      if action == 'stop':
                          ec2.stop_instances(InstanceIds=[instance_id])
                      else:
                          ec2.start_instances(InstanceIds=[instance_id])
                      started.append(instance_id)
                      succeeded.append(instance_id)
                  except Exception as e:
                      logger.error(f"{'停止' if action == 'stop' else '起動'}失敗: {instance_id} エラー: {e}")
              logger.info(f"{'停止' if action == 'stop' else '起動'}を開始したEC2インスタンス: {started}")
              logger.info(f"{'停止' if action == 'stop' else '起動'}が成功されたEC2インスタンス: {succeeded}")
              logger.info("")
          
          def process_rds_instances(instances, action):
              available, stopped, others = [], [], []
              matched_ids, started, succeeded = [], [], []
              for db in instances:
                  state = db['DBInstanceStatus']
                  line = f"- RDS: {db['DBInstanceIdentifier']} 状態: {convert_state(state)}"
                  if state == 'available':
                      available.append(line)
                  elif state == 'stopped':
                      stopped.append(line)
                  else:
                      others.append(line)
                  if db['Tags'].get(tag_key) == tag_value:
                      if (action == 'stop' and state == 'available') or (action == 'start' and state == 'stopped'):
                          matched_ids.append(db['DBInstanceIdentifier'])
              log_with_header("起動中RDS:", available)
              log_with_header("一時停止中RDS:", stopped)
              log_with_header("その他のステータスにあるRDSインスタンス:", others)
              logger.info(f"タグが一致する{('停止' if action == 'stop' else '起動')}対象RDS: {matched_ids}")
              for db_id in matched_ids:
                  try:
                      if action == 'stop':
                          rds.stop_db_instance(DBInstanceIdentifier=db_id)
                      else:
                          rds.start_db_instance(DBInstanceIdentifier=db_id)
                      started.append(db_id)
                      succeeded.append(db_id)
                  except Exception as e:
                      logger.error(f"RDS{'停止' if action == 'stop' else '起動'}失敗: {db_id} エラー: {e}")
              logger.info(f"{'停止' if action == 'stop' else '起動'}を開始したRDS: {started}")
              logger.info(f"{'停止' if action == 'stop' else '起動'}が成功されたRDS: {succeeded}")
              logger.info("")
          
          def process_aurora_clusters_v2(clusters, action):
              # Cluster states: 'available' for running, 'stopped' for stopped
              available, stopped, others = [], [], []
              matched_ids, started, succeeded = [], [], []
              for cluster in clusters:
                  state = cluster['Status']
                  line = f"- Aurora v2: {cluster['DBClusterIdentifier']} 状態: {convert_state(state)}"
                  if state == 'available':
                      available.append(line)
                  elif state == 'stopped':
                      stopped.append(line)
                  else:
                      others.append(line)
                  # Tag check and state check for action (Idempotency)
                  if cluster['Tags'].get(tag_key) == tag_value:
                      cluster_id = cluster['DBClusterIdentifier']
                      # Idempotency check: start only if stopped, stop only if available
                      if (action == 'stop' and state == 'available') or \
                         (action == 'start' and state == 'stopped'):
                          matched_ids.append(cluster_id)
              # Logging headers
              log_with_header("起動中Aurora Cluster v2:", available)
              log_with_header("一時停止中Aurora Cluster v2:", stopped)
              log_with_header("その他のステータスにあるAurora Cluster v2:", others)
              logger.info(f"タグが一致する{('停止' if action == 'stop' else '起動')}対象Aurora Cluster v2: {matched_ids}")
              # Execute action
              for cluster_id in matched_ids:
                  try:
                      if action == 'stop':
                          rds.stop_db_cluster(DBClusterIdentifier=cluster_id)
                      else:
                          rds.start_db_cluster(DBClusterIdentifier=cluster_id)
                      started.append(cluster_id)
                      succeeded.append(cluster_id)
                  except Exception as e:
                      logger.error(f"Aurora Cluster v2{'停止' if action == 'stop' else '起動'}失敗: {cluster_id} エラー: {e}")
              logger.info(f"{'停止' if action == 'stop' else '起動'}を開始したAurora Cluster v2: {started}")
              logger.info(f"{'停止' if action == 'stop' else '起動'}が成功されたAurora Cluster v2: {succeeded}")
              logger.info("")
          
          def process_asg_groups(groups, action):
              matched, succeeded = [], []
              act_msg = "スケールアップ" if action == 'start' else "スケールダウン"

              for grp in groups:
                  tags = {t['Key']: t['Value'] for t in grp.get('Tags', [])}
                  if tags.get('ChangeCapacityBylambda', '').lower() != 'true':
                      continue  # Skip if not enabled for Lambda management

                  name = grp['AutoScalingGroupName']
                  cur_min, cur_max, cur_des = grp['MinSize'], grp['MaxSize'], grp['DesiredCapacity']
                  matched.append(f"- ASG: {name}。現キャパシティー: Min={cur_min} Max={cur_max} Desired={cur_des}")

                  try:
                      if action == 'stop':
                          # On stop: use ScaleDownTo tag value
                          if 'ScaleDownTo' not in tags:
                              logger.error(f"ASG {name} に 'ScaleDownTo' タグがありません。スキップします。")
                              continue

                          new_cap = int(tags['ScaleDownTo'])

                          asg.update_auto_scaling_group(
                              AutoScalingGroupName=name,
                              MinSize=new_cap,
                              DesiredCapacity=new_cap
                              # MaxSize remains unchanged
                          )
                          succeeded.append(f"- ASG: {name}。新キャパシティー: Min={new_cap} Desired={new_cap} Max={cur_max}")

                      elif action == 'start':
                          # On start: restore from MinimumCapacity, MaximumCapacity, DesiredCapacity tags
                          new_min = int(tags['MinimumCapacity'])
                          new_max = int(tags['MaximumCapacity'])
                          new_desired = int(tags['DesiredCapacity'])

                          asg.update_auto_scaling_group(
                              AutoScalingGroupName=name,
                              MinSize=new_min,
                              MaxSize=new_max,
                              DesiredCapacity=new_desired
                          )
                          succeeded.append(
                              f"- ASG: {name}。新キャパシティー: Min={new_min} Max={new_max} Desired={new_desired}"
                          )

                      else:
                          logger.warning(f"不正なアクション: {action}")

                  except (KeyError, ValueError) as e:
                      logger.error(f"ASG {name} のタグの読み取りエラーまたは無効な値: {e}")
                  except Exception as e:
                      logger.error(f"ASG {act_msg}失敗: {name} エラー: {e}")

              log_with_header(f"{act_msg}する対象ASG:", matched)
              log_with_header(f"{act_msg}が成功されたASG:", succeeded)

          def stop_resources():
              ec2_instances = describe_ec2_instances()
              process_ec2_instances(ec2_instances, action='stop')
              rds_instances = describe_rds_instances()
              process_rds_instances(rds_instances, action='stop')
              aurora_clusters_v2 = describe_aurora_clusters_v2()
              process_aurora_clusters_v2(aurora_clusters_v2, action='stop')
              asg_groups = describe_asg_groups()
              process_asg_groups(asg_groups, action='stop')
          
          def start_resources():
              ec2_instances = describe_ec2_instances()
              process_ec2_instances(ec2_instances, action='start')
              rds_instances = describe_rds_instances()
              process_rds_instances(rds_instances, action='start')
              aurora_clusters_v2 = describe_aurora_clusters_v2()
              process_aurora_clusters_v2(aurora_clusters_v2, action='start')
              asg_groups = describe_asg_groups()
              process_asg_groups(asg_groups, action='start')
          
          def lambda_handler(event, context):
              logger.info(f"イベント受信: {json.dumps(event)}")
              action = event.get("action", "stop").lower()
              if action == "stop":
                  stop_resources()
              elif action == "start":
                  start_resources()
              else:
                  logger.warning(f"Unexpected Action: {action}")
              return {"statusCode": 200, "body": json.dumps({"action": action})}

  # CloudWatch Event Rule (EventBridge) to trigger Lambda for STOP
  InstancesAutoTriggerLambdaStopRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub "instances-auto-trigger-lambda-stopper"
      ScheduleExpression: !Ref InstancesStopTime
      State: !Ref StopController
      Targets:
        - Arn: !GetAtt InstancesAutoTriggerLambdaFunction.Arn
          Id: LambdaStopTarget
          Input: '{"action": "stop"}'

  # CloudWatch Event Rule (EventBridge) to trigger Lambda for START
  InstancesAutoTriggerLambdaStartRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub "instances-auto-trigger-lambda-starter"
      ScheduleExpression: !Ref InstancesStartTime
      State: !Ref StartController
      Targets:
        - Arn: !GetAtt InstancesAutoTriggerLambdaFunction.Arn
          Id: LambdaStartTarget
          Input: '{"action": "start"}'

  # Permission to allow CloudWatch to invoke the STOP lambda
  InstancesAutoTriggerLambdaInvokePermission4Stopper:
    Type: AWS::Lambda::Permission
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName: !Ref InstancesAutoTriggerLambdaFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt InstancesAutoTriggerLambdaStopRule.Arn

  # Permission to allow CloudWatch to invoke the START lambda
  InstancesAutoTriggerLambdaInvokePermission4Starter:
    Type: AWS::Lambda::Permission
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName: !Ref InstancesAutoTriggerLambdaFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt InstancesAutoTriggerLambdaStartRule.Arn

Outputs:
  InstancesAutoTriggerLambdaExecutionRoleArn:
    Description: 'Lambda Execution Role ARN'
    Value: !GetAtt InstancesAutoTriggerLambdaExecutionRole.Arn
    Export:
      Name: !Sub "instances-auto-trigger-lambda-iam-role-arn"
      
  InstancesAutoTriggerLambdaExecutionRoleName:
    Description: 'Lambda Execution Role Name'
    Value: !Ref InstancesAutoTriggerLambdaExecutionRole
    Export:
      Name: !Sub "instances-auto-trigger-lambda-iam-role-name"

  InstancesAutoTriggerLambdaFunctionArn:
    Description: 'Lambda Function ARN'
    Value: !GetAtt InstancesAutoTriggerLambdaFunction.Arn
    Export:
      Name: !Sub "instances-auto-trigger-lambda-arn"

  LambdaInstancesAutoTriggerLambdaFunctionName:
    Description: 'Lambda Function Name'
    Value: !Ref InstancesAutoTriggerLambdaFunction
    Export:
      Name: !Sub "instances-auto-trigger-lambda-name"

  LogGroupArn:
    Description: 'CloudWatch Log Group ARN'
    Value: !GetAtt InstancesAutoTriggerLambdaLogGroup.Arn
    Export:
      Name: !Sub "instances-auto-trigger-lambda-loggroup-arn"
      
  InstancesAutoTriggerLambdaLogGroupName:
    Description: 'CloudWatch Log Group Name'
    Value: !Ref InstancesAutoTriggerLambdaLogGroup
    Export:
      Name: !Sub "instances-auto-trigger-lambda-loggroup-name"

  InstancesAutoTriggerLambdaStopRuleArn:
    Description: 'CloudWatch Event Rule ARN (Stop)'
    Value: !GetAtt InstancesAutoTriggerLambdaStopRule.Arn
    Export:
      Name: !Sub "instances-auto-trigger-lambda-eventbridge-stop-rule-arn"

  InstancesAutoTriggerLambdaStopRuleName:
    Description: 'CloudWatch Event Rule Name (Stop)'
    Value: !Ref InstancesAutoTriggerLambdaStopRule
    Export:
      Name: !Sub "instances-auto-trigger-lambda-eventbridge-stop-rule-name"

  InstancesAutoTriggerLambdaStartRuleArn:
    Description: 'CloudWatch Event Rule ARN (Start)'
    Value: !GetAtt InstancesAutoTriggerLambdaStartRule.Arn
    Export:
      Name: !Sub "instances-auto-trigger-lambda-eventbridge-start-rule-arn"

  InstancesAutoTriggerLambdaStartRuleName:
    Description: 'CloudWatch Event Rule Name (Start)'
    Value: !Ref InstancesAutoTriggerLambdaStartRule
    Export:
      Name: !Sub "instances-auto-trigger-lambda-eventbridge-start-rule-name"

InstancesAutoTriggerChangeCapacityByLambdaMinimumCapacityMaximumCapacityDesiredCapacityScaleDownTo は、CloudFormation テンプレート内で使用されている仮の名前(プレースホルダー)です。実際の環境や命名規則に合わせて適宜変更してください。

⚠️ 注意
Auto Scaling Group のスケールダウンは EC2 インスタンスを**一時停止ではなく終了(terminate)**します。適用前に、対象インスタンスの AMI が取得済みであること、または再作成に必要な情報が揃っていることを必ず確認してください。

制御対象リソース

このテンプレートで制御できるリソースは以下の通りです:

  • Auto Scaling Group:キャパシティ変更(スケール)に対応
  • Aurora Cluster (v2):自動起動・停止に対応
  • RDS インスタンス:自動起動・停止に対応
  • EC2 インスタンス:自動起動・停止に対応

CloudFormationテンプレートのポイント

環境変数とパラメータによる動的な制御

パラメータ/環境変数 役割 実装の優位性
InstancesStartTime, InstancesStopTime EventBridgeのCRON式スケジュール。 テンプレートを変更せず、スタックのパラメータのみでスケジュールの調整が可能です。
StartController, StopController EventBridge RuleのState(有効/無効)制御。 自動化を一時的に停止する際、リソース削除が不要で安全です。

IAMポリシーにおける「最小権限の原則」

Lambdaの実行ロール(InstancesAutoTriggerLambdaExecutionRole)には、最小権限の原則に基づいたIAMポリシーが設定されています。

特に、EC2やRDSの操作(例:ec2:StopInstances, rds:StartDBInstance)には、リソースタグによる制限をかけています。これは Condition ブロックを用いて、対象リソースに特定のタグが付与されている場合のみ許可されるようにしています。

# CloudFormationテンプレートのIAMポリシー抜粋
Condition:
  StringEquals:
    aws:ResourceTag/InstancesAutoTrigger: "true"

この設定により、InstancesAutoTrigger: true タグが付いた EC2 や RDS インスタンスのみが、Lambda によって操作されます。
これにより、誤操作による影響を最小限に抑えつつ、セキュリティを強化することができます。これは、IAM設計におけるベストプラクティスのひとつです。

EventBridgeからの起動引数

EventBridge ルール(InstancesAutoTriggerLambdaStopRule / InstancesAutoTriggerLambdaStartRule)では、Lambda に渡すイベントペイロード(JSON)を明示的に定義しています。

# STOP Rule の設定例
Targets:
  - Arn: !GetAtt InstancesAutoTriggerLambdaFunction.Arn
    Id: LambdaStopTarget
    Input: '{"action": "stop"}'

Lambda関数は、このJSON内の action キーを参照し、
値に応じて start_resources() または stop_resources() を呼び出します。

これにより、1つのLambda関数で「起動」と「停止」の両方の処理を兼ねる、シンプルかつ効率的な構成を実現しています。

Lambda関数(Python)のロジック解説

ZipFile には、Lambda関数のコードがインラインで記述されています。

処理の共通化と日本語ロギング

各リソースの処理は、それぞれ専用の関数(例:process_ec2_instances)に分かれており、責務が明確に分離されています。
convert_state 関数を使用して、例えば running**起動中** のように、状態を日本語に変換してログに出力しています。

ログ出力例

以下は、各リソースに対して出力される日本語ログの一例です。

✅ EC2 インスタンスのログ
[INFO] 起動中EC2インスタンス:
- EC2: i-0abcd1234ef567890 状態: 起動中

[INFO] 停止中EC2インスタンス:
[]

[INFO] その他のステータスにあるEC2インスタンス:
- EC2: i-0123abcd4567efgh 状態: 一時停止

[INFO] タグが一致する停止対象EC2インスタンス: ['i-0abcd1234ef567890']
[INFO] 停止を開始したEC2インスタンス: ['i-0abcd1234ef567890']
[INFO] 停止が成功されたEC2インスタンス: ['i-0abcd1234ef567890']
✅ RDS インスタンスのログ
[INFO] 起動中RDS:
- RDS: my-rds-instance 状態: 起動中

[INFO] 一時停止中RDS:
[]

[INFO] その他のステータスにあるRDSインスタンス:
[]

[INFO] タグが一致する停止対象RDS: ['my-rds-instance']
[INFO] 停止を開始したRDS: ['my-rds-instance']
[INFO] 停止が成功されたRDS: ['my-rds-instance']
✅ Aurora Cluster v2 のログ
[INFO] 起動中Aurora Cluster v2:
- Aurora v2: my-aurora-cluster 状態: 起動中

[INFO] 一時停止中Aurora Cluster v2:
[]

[INFO] その他のステータスにあるAurora Cluster v2:
[]

[INFO] タグが一致する停止対象Aurora Cluster v2: ['my-aurora-cluster']
[INFO] 停止を開始したAurora Cluster v2: ['my-aurora-cluster']
[INFO] 停止が成功されたAurora Cluster v2: ['my-aurora-cluster']
✅ Auto Scaling Group (ASG) のログ
[INFO] スケールダウンする対象ASG:
- ASG: my-asg。現キャパシティー: Min=2 Max=4 Desired=3

[INFO] スケールダウンが成功されたASG:
- ASG: my-asg。新キャパシティー: Min=0 Desired=0 Max=4

ASGのキャパシティ変更ロジック

ASG(Auto Scaling Group)の制御には、UpdateAutoScalingGroup API を使用して、タグの値をもとにキャパシティを変更します。

# LambdaコードのASG処理抜粋
if action == 'stop':
    new_cap = int(tags['ScaleDownTo'])
    asg.update_auto_scaling_group(
        AutoScalingGroupName=name,
        MinSize=new_cap,
        DesiredCapacity=new_cap
        # MaxSize は変更しない
    )
elif action == 'start':
    new_min = int(tags['MinimumCapacity'])
    new_max = int(tags['MaximumCapacity'])
    new_desired = int(tags['DesiredCapacity'])
    asg.update_auto_scaling_group(
        AutoScalingGroupName=name,
        MinSize=new_min,
        MaxSize=new_max,
        DesiredCapacity=new_desired
    )
  • 対象の ASG には、タグ ChangeCapacityByLambda: true が付与されている必要があります。
  • 停止時(action == 'stop'
    タグ ScaleDownTo の値を使用して、MinSizeDesiredCapacity を変更します(MaxSize はそのままにします)。
  • 起動時(action == 'start'
    タグ MinimumCapacityMaximumCapacityDesiredCapacity の値を使用して、それぞれ設定します。

個々の EC2 インスタンスを直接操作せず、ASG のスケーリング設定を通じてインスタンス数をコントロールします。

タグによるリソースフィルタリング

process_* 関数内では、リソースに設定されたタグをチェックし、対象をフィルタリングしています。

たとえば、EC2の対象リソースを以下のように絞り込んでいます:

# EC2のタグフィルタリングロジック抜粋
if inst['Tags'].get(tag_key) == tag_value:
    if (action == 'stop' and state == 'running') or (action == 'start' and state == 'stopped'):
        matched_ids.append(inst['InstanceId'])

これにより、意図しないリソースへの操作を防ぎつつ、対象を必要なリソースに絞ることができます。
また、後の処理で状態変化や実行結果をログに残すため、動作の確認や問題発見がしやすくなっています。

🔄導入と利用方法

CloudFormationスタックのデプロイ

AWSマネジメントコンソール、AWS CLI、またはお好みのIaCツールでこのテンプレートをデプロイしてください。

重要な設定:

  • InstancesStartTimeInstancesStopTime のCron式を、ご自身の業務スケジュールとUTC時間に合わせて調整してください。

    例: 毎日午前9時(JST)に起動したい場合は cron(0 0 ? * * *) (UTC 0時 = JST 9時)

  • Auto Scaling Groupのキャパシティー AsgCapacityUP / AsgCapacityDOWN はニーズに合わせて調整してください。

リソースへのタグ付け

自動起動/停止の対象にしたいEC2、RDS、Aurora Cluster V2には以下のタグを付与してください。

Key Value
InstancesAutoTrigger true

自動でスケール調整したいAuto Scaling Groupには、以下のタグを付けてください。

Key Value
ChangeCapacityByLambda true
ScaleDownTo (停止時の容量)
MinimumCapacity (起動時の最小容量)
MaximumCapacity (起動時の最大容量)
DesiredCapacity (起動時の希望容量)

🚀 まとめ

本記事では、タグを活用して、EC2やRDS、Aurora、オートスケーリンググループの起動・停止およびスケール調整を自動化する方法をご紹介しました。LambdaとEventBridgeを組み合わせることで、起動停止の自動化が可能となり、運用効率の向上やコスト削減にもつながります。ご紹介したサンプルテンプレートは、インスタンスIDやDB識別子、オートスケーリンググループ名に依存しない、比較的シンプルな構成です。環境やニーズに応じてそのまま利用いただくことも、カスタマイズして活用いただくことも可能です。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?