EC2インスタンスを自動で起動/停止する

作った機能

  • EC2サーバーを指定時刻に起動、停止する機能を作成しました
  • アカウントに依存することなく、パラメータを注入すればコピペで実行可能な機能にしてみました。
  • あくまでお遊び機能で保証などはできませんので、利用する責任はご自身でお願いいたします

背景

  • EC2インスタンスがあるのですが、サーバー自体、24時間365日起動しているのは勿体無いなと思いました。
  • 単純に考えると24時間起動→12時間起動にするだけで、コストを50%近く削減できます。
  • 昨今は、為替の影響等でクラウドコストが高騰しているので、みなさん同じ課題をお持ちかと思いました。

構成について

以下の構成で機能を構築しました。

AWS構成図
  • 起動/停止のスケジュールは、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形式で圧縮してください

※上記のように、auto-start-stopといったフォルダを作成して各ファイルを配置すると、分かりやすくて🙆

機能のデプロイ

  • Cloudformationスタックの作成 -> アップロードしたCFnテンプレートファイルのURLを入力
  • 次に、パラメータを設定します
    • S3BucketName
      • pythonコードを配置したS3バケット名称を設定してください
    • S3ObjectKey
      • pythonコードのS3オブジェクトのキーを設定してください
    • StartCronExpression
    • StartInstanceNames
      • 起動対象EC2インスタンスのNameを指定してください。
      • 複数ある場合は「,」区切りで記載してください。
    • StopCronExpression
      • 停止時間をUTCのCron形式で記載してください。
      • もし、日本時間(JST)の午後11時を指定する場合は、「
        cron(0 14 * * ? *)」のように記載できます
    • StopInstanceNames
      • 停止対象EC2インスタンスのNameを指定してください。
      • 複数ある場合は「,」区切りで記載してください。
  • スタックオプションの設定

特に設定をする必要はありません。

ただ、IAMリソースを作成する場合、以下の確認ダイアログがでてくるので、☑️をいれましょう

  • 確認して作成

確認画面で設定値を確認後、作成を実行してください。

※もし、ここで実行に失敗する場合は利用している権限が不足している可能性があります。

  • スタックの作成完了を確認

ステータスが「CREATE_COMPLETE」になれば完了です。

その他

  • 機能自体削除したい場合は、Cloudformationの機能を利用して削除可能です。
  • 起動/停止に関する対象EC2、スケジュールを変更したい場合は、「直接更新を実行」から変更ができます。

最後に

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