EC2とRDSを自動停止!AWSコスト削減のためのスクリプトを紹介

AWSのEC2やRDS、検証環境でついつい止め忘れて、予想外の請求額にヒヤッとした経験はありませんか?

そんな悩みを解決するために、 EC2とRDSを自動で停止するスクリプト を作成しました!

このスクリプトを使えば、毎晩決まった時間にEC2とRDSを自動で停止してくれるので、コスト削減と運用効率の向上に繋がります。

スクリプトの特徴

  • 簡単設定: Cloudshellで数コマンドを実行するだけで、全リージョンまたは特定リージョンにスクリプトをデプロイできます。
  • 停止設定: 日本時間の午後9時から午前8時まで、1時間おきに停止スクリプトを実行し、起動中のEC2とRDSを確実に停止します。
  • 停止除外設定: 特定のEC2やRDSを常時稼働させたい場合は、タグを設定するだけで停止対象から除外できます。

スクリプトの使い方

  1. ファイルをダウンロード: 以下のコマンドをCloudshellで実行して、スクリプトをダウンロードします。
    wget https://iret.media/wp-content/uploads/2025/02/Automatic-Stop-System.zip
  2. ファイルを解凍:
    unzip Automatic-Stop-System.zip
  3. ディレクトリに移動:
    cd Automatic-Stop-System
  4. スクリプトを実行:
    • 全リージョンにデプロイする場合:
      python Deploy_All_Region.py
    • 特定のリージョンにデプロイする場合:
      python Deploy_Individual_Region.py
    • デプロイしたスタックを削除する場合:
      python Delete_All_Region.py
  5. 特定のEC2・RDSを停止から除外:
    停止したくないEC2・RDSインスタンス・RDSクラスターに、以下のタグを追加します。
    • キー: Always_Running_Reason
    • 値: 常時起動する理由を英語で記載 (例: For monitoring)

構成要素

このスクリプトは、以下の要素で構成されています。

  • デプロイ用Pythonスクリプト: CloudFormationスタックの作成・更新、Lambda関数のデプロイを自動化します。
  • CloudFormationテンプレート: Lambda関数、IAMロール、CloudWatch EventsルールなどのAWSリソースを定義します。
  • Lambda関数: EC2インスタンス、RDSクラスタ、RDSインスタンスを停止する処理を実行します。

●デプロイ用Pythonスクリプトの説明

デプロイ用Pythonスクリプトのフローチャートは以下となります。S3からLambda関数をデプロイする場合、LambdaとS3は同一リージョンに存在する必要があるため、デプロイするリージョンごとにS3バケットを作成しています。

作成されたS3バケットのバケット名とキー情報はCfnのパラメータとしてデプロイ時に参照されます。

フローチャート

●CFnテンプレート及びLambda関数の説明

CFnテンプレートでは、各リージョンに「EventBridge Scheduler」と「Lambda関数」をデプロイするシンプルな構成としています。

AWSTemplateFormatVersion: '2010-09-09'
Description: 'Automatic-Stop-System-Deploy'
Parameters:
  TempS3BucketName:
    Type: String
  TempS3KeyName:
    Type: String

Resources:
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
      Policies:
        - PolicyName: !Sub '${AWS::Region}-Automatic-Stop-System-Lambda-Policy'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:DescribeInstances
                  - ec2:StopInstances
                  - rds:DescribeDBClusters
                  - rds:StopDBCluster
                  - rds:DescribeDBInstances
                  - rds:StopDBInstance
                Resource: '*'

  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.lambda_handler
      Code:
        S3Bucket: !Sub '${TempS3BucketName}'
        S3Key: !Sub '${TempS3KeyName}'
      Role: !GetAtt LambdaFunctionRole.Arn
      Runtime: python3.11
      MemorySize: 128
      Timeout: 600

  LogGroupFirehosLambda:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/lambda/${LambdaFunction}'
      RetentionInDays: 7

  LambdaFunctionPermission:
    Type: "AWS::Lambda::Permission"
    Properties:
      Action: "lambda:InvokeFunction"
      FunctionName: !GetAtt 
        - LambdaFunction
        - Arn
      Principal: "events.amazonaws.com"
      SourceArn: !GetAtt 
        - StopScheduleEvent
        - Arn

  StopScheduleEvent:
    Type: AWS::Events::Rule
    Properties:
      Description: ’Stop schedule event for lambda’
      ScheduleExpression: 'cron(0 12-23 * * ? *)'
      State: ENABLED
      Targets:
        - Arn: !GetAtt LambdaFunction.Arn
          Id: ScheduleEvent1Target
    DependsOn:
      - LambdaFunction

Lambda関数では対象リソースに設定されたタグ情報を読み込み、除外対象のリソースの場合は処理をスキップさせます。

import boto3, json
from botocore.exceptions import ClientError

client_EC2 = boto3.client('ec2')
client_RDS = boto3.client('rds')

def Stop_EC2_instances():
    try:
        response = client_EC2.describe_instances(Filters=[{'Name': 'instance-state-name','Values': ['running']}])
    except ClientError as e:
        print('boto3 client error: %s' % e)
        return

    for reservation in response['Reservations']:

        flag = False
        for tag in reservation['Instances'][0]['Tags']:
            if(tag['Key'] == 'Always_Running_Reason' and tag['Value']):
                flag = True
        if flag:
            continue

        try:
            response = client_EC2.stop_instances(InstanceIds=[reservation['Instances'][0]['InstanceId']])
        except ClientError as e:
            print('boto3 client error: %s' % e)
            continue

def Stop_DB_Cluster():
    try:
        response = client_RDS.describe_db_clusters()
    except ClientError as e:
        print('boto3 client error: %s' % e)
        return

    for DBCluster in response['DBClusters']:
        if not DBCluster['Status'] == "available":
            continue
        if not "aurora" in DBCluster['Engine']:
            continue

        flag = False
        for tag in DBCluster['TagList']:
            if(tag['Key'] == 'Always_Running_Reason' and tag['Value']):
                flag = True
        if flag:
            continue

        try:
            response = client_RDS.stop_db_cluster(DBClusterIdentifier=DBCluster['DBClusterIdentifier'])
        except ClientError as e:
            print('boto3 client error: %s' % e)
            continue

def Stop_DB_Instance():
    try:
        response = client_RDS.describe_db_instances()
    except ClientError as e:
        print('boto3 client error: %s' % e)
        return

    for DBInstance in response['DBInstances']:
        if not DBInstance['DBInstanceStatus'] == "available":
            continue
        if "aurora" in DBInstance['Engine']:
            continue

        flag = False
        for tag in DBInstance['TagList']:
            if(tag['Key'] == 'Always_Running_Reason' and tag['Value']):
                flag = True
        if flag:
            continue
        try:
            response = client_RDS.stop_db_instance(DBInstanceIdentifier=DBInstance['DBInstanceIdentifier'])
        except ClientError as e:
            print('boto3 client error: %s' % e)
            continue

def lambda_handler(event, content):
    Stop_EC2_instances()
    Stop_DB_Cluster()
    Stop_DB_Instance()
    return { 'statusCode': 200,'body': json.dumps('Excuted!')}

このスクリプトで、AWSコストの削減と運用効率の向上を実現しましょう!