前回DifyでCertbotをつかったSSLにふれました。

https://smooz.cloud/news/column/dify-free-ssl/

ALBを使わないのでコストを抑えることが可能です。
また、以前第3回でEC2を再起動しても自動起動するように設定してもあります。
今回はこれを利用してさらにコストを安くしようという内容です。

Difyを有料で使おうとすると、Dify直のサブスク利用をするという方法があるのですが、月59ドルなので150円換算で1万弱します。
他にはXServerが提供する「Xserver VPS」を使えば実質850円で使えるというのもあります。

Xserver VPS 料金・仕様一覧: https://vps.xserver.ne.jp/price.php

ただこれ36か月契約したときの実質料金なんですよね。
画像の通り、1か月ごと契約だと2200円です。
もちろんマネージドでインストールの手間とかないのはメリットです。

xserver-price

これをいかにAWSで近づけるか。という感じです。

ただし条件として、Difyを使う際に例えばDifyをAPIとして利用し公に公開し続けるなら365日つけときたいです。その場合で長期利用するならリザーブドインスタンス一択です。

そうではなく

・社内での勉強利用
・社内でのチャット利用
・PoC利用

というのであれば平日だけで問題ないと思うんですよね。
更に深夜も使うことはない。つまり平日勤務であれば、月~金の例えば10時~18時で利用を停止するとかで十分だと。それ以外は自分でつけたり停止したり。

そうなれば、少なくても土日の2/7が停止できて、さらに一日の内の2/3もEC2を停止できます。
オンデマンドインスタンスのままで停止して、40時間稼働/168時間とすると24%相当の稼働で済みます。AWSの長期割引のリザーブドインスタンスよりも明らか安くなります。

逆にプライベートで使いたいとして、逆に勤務時間中は使うことないので勤務時間中止めておいたり深夜止めておくとかすればEC2負担分をかなり安くできます。

早速やってみましょう。

今回達成できること

AWS LambdaAmazon EventBridgeを使って時間で自動でEC2(Dify)を停止・起動をするようにする

1.EC2(Dify)の停止と起動のLambda関数を作る

今回はEC2を停止するLambda関数と起動するLambda関数の合計2個のLambdaを作り、それをイベントブリッジのスケジュールで操作する感じです。まずはLambda関数を作るところからスタートします。

Dify_ec2_stop
Dify_ec2_start

今回関数名は上記の名前で行きたいと思います。

dify-lambda1

ランタイムはPython 3.10で。

dify-lambda2

続いてコードのコードソースのところに以下のコードを張り付けてください。コピペで行けます。

EC2(Dify)停止するLambda

# Dify_ec2_stop

import boto3
import os
import logging

# ロギングの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# EC2クライアントの初期化
ec2 = boto3.client('ec2')

# 環境変数からインスタンスIDを取得(Lambda関数の環境変数で設定)
def get_instance_id(event):
    return event.get('instance_id', os.environ['INSTANCE_ID'])

def lambda_handler(event, context):
    # 動的にインスタンスIDを取得
    INSTANCE_ID = get_instance_id(event)
    
    try:
        # インスタンスの現在の状態を確認
        logger.info(f'EC2インスタンスの状態を取得中: {INSTANCE_ID}')
        response = ec2.describe_instances(InstanceIds=[INSTANCE_ID])
        state = response['Reservations'][0]['Instances'][0]['State']['Name']
        
        if state == 'running':
            # インスタンスが実行中の場合、停止を開始
            ec2.stop_instances(InstanceIds=[INSTANCE_ID])
            logger.info(f'EC2インスタンスを停止中: {INSTANCE_ID}')
            return {
                'statusCode': 200,
                'headers': {
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Methods': 'POST'
                },
                'body': f'EC2インスタンスを停止中: {INSTANCE_ID}'
            }
        elif state == 'stopped':
            # 既に停止している場合
            logger.info(f'EC2インスタンス {INSTANCE_ID} は既に停止しています。')
            return {
                'statusCode': 200,
                'headers': {
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Methods': 'POST'
                },
                'body': f'EC2インスタンス {INSTANCE_ID} は既に停止しています。'
            }
        elif state == 'pending':
            # インスタンスが起動処理中の場合
            logger.info(f'EC2インスタンス {INSTANCE_ID} は起動中(pending)です。停止処理は行われません。')
            return {
                'statusCode': 200,
                'headers': {
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Methods': 'POST'
                },
                'body': f'EC2インスタンス {INSTANCE_ID} は起動中(pending)です。操作は行いません。'
            }
        else:
            # その他の状態(停止中など)の場合
            logger.info(f'EC2インスタンス {INSTANCE_ID} は {state} 状態です。操作は行いません。')
            return {
                'statusCode': 200,
                'headers': {
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Methods': 'POST'
                },
                'body': f'EC2インスタンス {INSTANCE_ID} は {state} 状態です。操作は行いません。'
            }
    
    except Exception as e:
        # エラーが発生した場合
        logger.error(f'エラーが発生しました: {str(e)}')
        return {
            'statusCode': 500,
            'headers': {
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Methods': 'POST'
            },
            'body': f'エラーが発生しました: {str(e)}'
        }
dify-lambda3

最初にあるテストコードは消してそのまま張り付けてください。

環境変数設定

続いてタブの設定に行き環境変数のところに「INSTANCE_ID」という名前で自分の制御かけたいEC2のIDを入力してください。

dify-lambda4

IAMポリシー設定

続いてIAMポリシーです。

dify-lambda5-accsess

設定のアクセス権限のロール名をクリックしてIAMポリシーを追加していきます。
今回LambdaがEC2を操作するのでLambdaがEC2を操作できる許可をつけます。ロール名をクリックしてIAMの設定画面へ。

dify-lambda6

インラインポリシーを作成をクリックして「JSON」を選択。以下のポリシーを張り付けてください。これもコピペで行けます。

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": [
				"ec2:DescribeInstances",
				"ec2:StopInstances"
			],
			"Resource": "*"
		}
	]
}

作成イメージはこんな感じ。

dify-lambda7

そうしたら次へをおしてポリシー名を決めてください。問われるので任意の適当な値を。今回は「Dify_ec2_stop」としました。Resourceはとりあえずワイルドカード使いましたがここでInstance名やタグを使えばさらに強固なポリシーを作れます。

dify-lambda8

こんな感じ。これでOKです。作成をしてLambdaに戻ってください。

テストをします。

今回環境変数さえ設定されていれば動くようになっているのでテストの値は空でOKです。

dify-lambda9

こんな感じで。テストを押します。

dify-lambda10

うまくいきましたね!200番がでてればOKです。EC2が動いていれば上記のようになります。

同じことをDify_ec2_startという関数をつくって行う。

これと同様のことをDify_ec2_startという関数でやってください。
Lambdaもポリシーも微妙に違いますので以下がstart用のコードです。コピペいけます。

EC2(Dify)起動するLambda

# Dify_ec2_start

import boto3
import os
import logging

# ロギングの設定
logger = logging.getLogger()
logger.setLevel(logging.WARNING)  # 基本はWARNINGレベルに設定

# EC2クライアントの初期化
ec2 = boto3.client('ec2')

# 環境変数からインスタンスIDを取得(Lambda関数の環境変数で設定)
INSTANCE_ID = os.environ['INSTANCE_ID']

def lambda_handler(event, context):
    logger.info('Lambda関数が呼び出されました。')  # Lambdaの呼び出しログ

    try:
        # インスタンスの状態を取得しようとしている部分のログ
        logger.info(f'EC2インスタンスの状態を取得中: {INSTANCE_ID}')
        
        # インスタンスの現在の状態を確認
        response = ec2.describe_instances(InstanceIds=[INSTANCE_ID])
        state = response['Reservations'][0]['Instances'][0]['State']['Name']
        
        if state == 'stopped':
            # インスタンスが停止中の場合、起動を開始
            ec2.start_instances(InstanceIds=[INSTANCE_ID])
            logger.info(f'EC2インスタンスを起動中: {INSTANCE_ID}')  # 状況確認ログ
            logger.info(f'EC2のstart_instancesコマンドが正常に送信されました: {INSTANCE_ID}')  # 起動コマンドの成功ログ
            return {
                'statusCode': 200,
                'headers': {
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Methods': 'POST'
                },
                'body': f'EC2インスタンスを起動中: {INSTANCE_ID}'
            }
        elif state == 'running':
            # 既に起動している場合は警告としてログ出力
            logger.warning(f'EC2インスタンス {INSTANCE_ID} は既に起動しています。')
            return {
                'statusCode': 200,
                'headers': {
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Methods': 'POST'
                },
                'body': f'EC2インスタンス {INSTANCE_ID} は既に起動しています。'
            }
        elif state == 'pending':
            # 起動処理中(pending)の状態を記録
            logger.info(f'EC2インスタンス {INSTANCE_ID} は起動中(pending)です。起動を待っています。')
            return {
                'statusCode': 200,
                'headers': {
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Methods': 'POST'
                },
                'body': f'EC2インスタンス {INSTANCE_ID} は起動中(pending)です。起動を待っています。'
            }
        else:
            # その他の状態(起動中など)の場合は警告
            logger.warning(f'EC2インスタンス {INSTANCE_ID} は {state} 状態です。操作は行いません。')
            return {
                'statusCode': 200,
                'headers': {
                    'Access-Control-Allow-Origin': '*',
                    'Access-Control-Allow-Methods': 'POST'
                },
                'body': f'EC2インスタンス {INSTANCE_ID} は {state} 状態です。操作は行いません。'
            }
    
    except Exception as e:
        # エラーが発生した場合は詳細なエラーログを出力
        logger.error(f'エラーが発生しました: {str(e)}', exc_info=True)
        return {
            'statusCode': 500,
            'headers': {
                'Access-Control-Allow-Origin': '*',
                'Access-Control-Allow-Methods': 'POST'
            },
            'body': f'エラーが発生しました: {str(e)}'
        }

startポリシー設定用のJSONはこちら。

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": [
				"ec2:DescribeInstances",
				"ec2:StartInstances"
			],
			"Resource": "*"
		}
	]
}

同じように環境変数設定設定できていれば200がでるはずなのと実際にEC2を見るとちゃんと停止、開始がされていると思います。

2.Amazon EventBridgeでこのLambdaを時間制御する

ではこれでLambdaを起動さえさせれば停止起動がEC2の画面に行かなくてもできるようになりました。
これをEventBridgeのスケジュール機能をつかって自動で時間制御を行います。

dify-lambda11

EventBridgeの画面に行き、サイドバースケジュールをおして、そのあとスケジュールを作成をクリックします。今回は

Dify_stop_time
Dify_start_time

という名前で作っていきます。これも二つ設定をします。まず停止から設定は以下の通りです。
cronの時間設定のところで実行する時間を設定できます。スクショは毎日18時に停止するようになっているものです。

例えば

dify-lambda12

このように書けば「月曜から金曜の毎日10時に実行」ということになります。ここはお好きな設定を考えてください。

EventBridgeを設定する

dify-lambda13

次へをおしてテンプレート化されたターゲットでLambda Invokeを選択。先ほど作ったLambdaを選択。

dify-lambda-event

次へをおして次の設定の画面は特になにも変更せず次へ。
スケジュールの確認と作成画面も特に問題なければ完了してください。

スケジュールに作成できてればOKです!これをstartもやっていきます。Dify_start_timeで作っていきます。

時間は

0 18 ? * MON-FRI *
0 10 ? * MON-FRI *

で各種つくりました。曜日を指定したときは日時のところが「?」になりますので注意を。

これで完了です。あとは時間通りに動いているかを確認してください。

次回予告

これで時間制御ができ、第3回の自動起動設定が活きてきました!
次はDifyのアップデートが来たときの対応を学んでおきましょう。

この記事を書いた人

スムーズロゴ
SMOOZ
enterprise-smooz

資料
ダウンロード

マスタデータのメンテナンスに関わる機能をまとめたSaaS「SMOOZ
SMOOZはリレーショナルデータベースの課題を解決するサービスです。
オンラインデモで気軽に試すことが可能です。