💡 はじめに
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を活用し、タグを利用したインスタンスの停止・起動・キャパシティー変更を自動化する仕組みを構築する方法を解説します。
🛠️ 構築するリソース
- Lambda関数の作成:
- EC2、RDS、ASGの状態を取得し、特定のタグを持つリソースに対して起動・停止・キャパシティー変更(スケールアップ/ダウン)を実行します。
- Pythonで実装されており、Boto3を使用して各サービスを操作します。
-
EventBridgeルールの作成:
EventBridgeで、特定の時間(例えば毎日午後10時)にLambdaをトリガーするスケジュール(Cron式で指定)を設定します。
-
AWS EventBridge Rule (Start): インスタンスを起動・スケールアップするためのスケジュールルール。
-
AWS EventBridge Rule (Stop): インスタンスを停止・スケールダウンするためのスケジュールルール。
-
IAMロールの設定:
Lambdaに必要な最小限の権限(EC2/RDS/ASGの操作、CloudWatch Logsへの書き込み)を付与します。 -
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"
InstancesAutoTrigger、ChangeCapacityByLambda、MinimumCapacity、MaximumCapacity、DesiredCapacity、ScaleDownTo は、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の値を使用して、MinSizeとDesiredCapacityを変更します(MaxSizeはそのままにします)。 -
起動時(
action == 'start')
タグMinimumCapacity、MaximumCapacity、DesiredCapacityの値を使用して、それぞれ設定します。
個々の 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ツールでこのテンプレートをデプロイしてください。
重要な設定:
-
InstancesStartTimeとInstancesStopTimeの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識別子、オートスケーリンググループ名に依存しない、比較的シンプルな構成です。環境やニーズに応じてそのまま利用いただくことも、カスタマイズして活用いただくことも可能です。