EC2インスタンスを自動で起動/停止する
作った機能
- EC2サーバーを指定時刻に起動、停止する機能を作成しました
- アカウントに依存することなく、パラメータを注入すればコピペで実行可能な機能にしてみました。
- あくまでお遊び機能で保証などはできませんので、利用する責任はご自身でお願いいたします
背景
- EC2インスタンスがあるのですが、サーバー自体、24時間365日起動しているのは勿体無いなと思いました。
- 単純に考えると24時間起動→12時間起動にするだけで、コストを50%近く削減できます。
- 昨今は、為替の影響等でクラウドコストが高騰しているので、みなさん同じ課題をお持ちかと思いました。
構成について
以下の構成で機能を構築しました。

- 起動/停止のスケジュールは、EventBridgeで管理
- EC2インスタンスの起動/停止はLambda関数(単一)で処理
- 起動/停止の対象は、EC2インスタンスのNameタグで管理
- Cloudformationテンプレートにすることで、デプロイだけでなく、パラメータの変更や機能自体の削除もできる
作成リソース
Cloudformationテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
S3BucketName:
Type: String
Description: Name of the S3 bucket where the Lambda code (zip) is stored
S3ObjectKey:
Type: String
Description: S3 object key for Lambda code (zip)
StartInstanceNames:
Type: String
Description: Name tag values to launch (comma separated)
StopInstanceNames:
Type: String
Description: Name tag values to be stopped (comma separated)
StartCronExpression:
Type: String
Description: Startup schedule (cron)
StopCronExpression:
Type: String
Description: Stop schedule (cron)
Resources:
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: ec2-start-stop-policy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- ec2:DescribeInstances
- ec2:StartInstances
- ec2:StopInstances
Resource: "*"
Ec2StartStopFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub ec2-start-stop-${AWS::StackName}
Runtime: python3.13
Handler: lambda_function.lambda_handler
Role: !GetAtt LambdaExecutionRole.Arn
Code:
S3Bucket: !Ref S3BucketName
S3Key: !Ref S3ObjectKey
Environment:
Variables:
START_INSTANCE_NAMES: !Ref StartInstanceNames
STOP_INSTANCE_NAMES: !Ref StopInstanceNames
StartScheduleRule:
Type: AWS::Events::Rule
Properties:
Name: !Sub ec2-start-schedule-${AWS::StackName}
ScheduleExpression: !Ref StartCronExpression
State: ENABLED
Targets:
- Id: lambda-start-target
Arn: !GetAtt Ec2StartStopFunction.Arn
Input: '{"action":"start"}'
StopScheduleRule:
Type: AWS::Events::Rule
Properties:
Name: !Sub ec2-stop-schedule-${AWS::StackName}
ScheduleExpression: !Ref StopCronExpression
State: ENABLED
Targets:
- Id: lambda-stop-target
Arn: !GetAtt Ec2StartStopFunction.Arn
Input: '{"action":"stop"}'
PermissionForEventsToInvokeLambdaFromStart:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref Ec2StartStopFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt StartScheduleRule.Arn
PermissionForEventsToInvokeLambdaFromStop:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref Ec2StartStopFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt StopScheduleRule.Arn
Outputs:
LambdaFunctionArn:
Value: !GetAtt Ec2StartStopFunction.Arn
Description: The ARN of the Lambda function that was created
StartRuleArn:
Value: !GetAtt StartScheduleRule.Arn
Description: EventBridge rule ARN for launch schedule
StopRuleArn:
Value: !GetAtt StopScheduleRule.Arn
Description: EventBridge rule ARN for the outage schedule
pythonコード
import os
import boto3
ec2 = boto3.client("ec2")
def _split_names(value: str) -> list[str]:
if not value:
return []
return [x.strip() for x in value.split(",") if x.strip()]
def _find_instance_ids_by_names(name_values: list[str], state_names: list[str]) -> list[str]:
if not name_values:
return []
paginator = ec2.get_paginator("describe_instances")
filters = [
{"Name": "tag:Name", "Values": name_values},
{"Name": "instance-state-name", "Values": state_names},
]
instance_ids: list[str] = []
for page in paginator.paginate(Filters=filters):
for reservation in page.get("Reservations", []):
for instance in reservation.get("Instances", []):
instance_id = instance.get("InstanceId")
if instance_id:
instance_ids.append(instance_id)
return instance_ids
def lambda_handler(event, context):
action = (event or {}).get("action")
start_names = _split_names(os.getenv("START_INSTANCE_NAMES", ""))
stop_names = _split_names(os.getenv("STOP_INSTANCE_NAMES", ""))
if action == "start":
target_ids = _find_instance_ids_by_names(start_names, ["stopped"])
if target_ids:
ec2.start_instances(InstanceIds=target_ids)
return {"action": action, "target_count": len(target_ids)}
if action == "stop":
target_ids = _find_instance_ids_by_names(stop_names, ["running"])
if target_ids:
ec2.stop_instances(InstanceIds=target_ids)
return {"action": action, "target_count": len(target_ids)}
return {"action": action, "target_count": 0}
機能のデプロイ方法
事前作業
- 各コードをS3バケットにupload(S3バケットは適宜作成ください)
- Cloudformationテンプレート
- ファイル名:任意(例:CFN-Template.yaml)
- pythonコード
- ファイル名:lambda_function.zip
- Lambda関数にアップロードするコードはzip形式で圧縮してください
- Cloudformationテンプレート

※上記のように、auto-start-stopといったフォルダを作成して各ファイルを配置すると、分かりやすくて🙆
機能のデプロイ
- Cloudformationスタックの作成 -> アップロードしたCFnテンプレートファイルのURLを入力

- 次に、パラメータを設定します
- S3BucketName
- pythonコードを配置したS3バケット名称を設定してください
- S3ObjectKey
- pythonコードのS3オブジェクトのキーを設定してください
- StartCronExpression
- 起動時間をUTCのCron形式で記載してください。
- もし、日本時間(JST)の午前8時を指定する場合は、「cron(0 23 * * ? *)」のように記載できます
- https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-scheduled-rule-pattern.html
- StartInstanceNames
- 起動対象EC2インスタンスのNameを指定してください。
- 複数ある場合は「,」区切りで記載してください。
- StopCronExpression
- 停止時間をUTCのCron形式で記載してください。
- もし、日本時間(JST)の午後11時を指定する場合は、「
cron(0 14 * * ? *)」のように記載できます
- StopInstanceNames
- 停止対象EC2インスタンスのNameを指定してください。
- 複数ある場合は「,」区切りで記載してください。
- S3BucketName

- スタックオプションの設定
特に設定をする必要はありません。
ただ、IAMリソースを作成する場合、以下の確認ダイアログがでてくるので、☑️をいれましょう

- 確認して作成
確認画面で設定値を確認後、作成を実行してください。
※もし、ここで実行に失敗する場合は利用している権限が不足している可能性があります。
- スタックの作成完了を確認
ステータスが「CREATE_COMPLETE」になれば完了です。

その他
- 機能自体削除したい場合は、Cloudformationの機能を利用して削除可能です。

- 起動/停止に関する対象EC2、スケジュールを変更したい場合は、「直接更新を実行」から変更ができます。

最後に
- 意外と小さなサーバーでも積み重ねで大きな金額になってしまいます。
- 為替を変動させるか、この機能を作成するか悩んだのですが、今回は機能作成することを選びました。
- 今後は為替を変えてみようかな?
