はじめに
こんにちは。株式会社DIVXのエンジニア、大久保です。
AWS EC2コスト削減の具体策を、株式会社DIVXが開発したAIシステム「DIVX GAI」を活用して実践します。
早速、DIVX GAIを活用し、効果的なAWS EC2コスト削減策を探っていきましょう。
使用するプロンプト
このプロンプトを使用して、AIに提案してもらいます。
インスタンスタイプや、使用状況、使用時間や除外事項を条件に指定し、送信しました。
すると下記の文章が返ってきました。
AIの提案が間違っていないか、確認しながら実行していこうと思います。
AIによるコスト削減提案のアプローチ
AWS EC2のステージング環境のコストを削減するためには、現状の使用状況や条件を考慮し、以下の3つのアプローチを検討できます。
1. スケジューリングによるインスタンスの自動停止
使用時間が平日9時〜20時に限定されているため、それ以外の時間でインスタンスを停止することでコストを削減できます。
アクションプラン:
CloudWatchとLambdaを使った自動スケジュール設定
CloudWatch Eventsでスケジュールルールを作成し、平日9時にインスタンスを開始、20時に停止するトリガーを設定。
AWS Lambda関数を作成し、EC2インスタンスの開始と停止を行うようにする。
スケジュールルールとLambda関数を紐付けることで、自動化を実現。
2. 使用頻度に応じたスケールダウン
平均CPU使用率とメモリ使用率を考慮し、インスタンスタイプをより小さいものに変更します。
アクションプラン:
t2.nanoやt3.microへの変更
現在のt2.smallのインスタンスを停止し、必要なAMIを作成。
新たなインスタンスタイプ(t2.nanoまたはt3.micro)でインスタンスを起動。
起動後、動作確認を行い、問題がなければステージング環境を運用。
3. 無停止の日を有効活用するためのスケジュールの最適化
使用しない日がある場合、それらの日を事前に把握し、インスタンスを停止させることでコストを削減します。
アクションプラン:
特定の休止日のスケジュール管理
チームでステージング環境を必要としない日をカレンダーで共有。
スケジュールされた停止日には手動、もしくは事前設定したCloudWatch Eventsでインスタンスを停止。
不要な日を考慮し、CloudWatch Eventsでインスタンスが自動的にオフラインになるようにしておく。
これらの提案により、AWS EC2のステージング環境のコストを効率的に削減することが可能です。それぞれのステップをチームと共有し、実行計画を確定することが重要です。
1. スケジューリングによるインスタンスの自動停止
まず1つ目はスケジューリングによるインスタンスの自動停止を提案されました。
公式のドキュメントも確認してみます。
AWSのコストを削減する9の方法のAmazon Web Services ブログでも迅速なコスト最適化が実現できる方法として言及していて、確かに使用時間外は停止することが推奨されています。
> 未使用状態のEC2やRDS インスタンスへの支払いを止める。開発環境、テスト環境などの本番環境以外で実行されているワークロードや、ミッションクリティカルでないワークロードなどにおいて、夜間、週末および祝日で、利用していない時間帯のEC2やRDSの費用を支払っている可能性があります。
>
上記のように未使用の状態のEC2を止める必要があることがわかりましたので、実際にアクションプランを実行していきます。
AI アクションプラン
CloudWatchとLambdaを使った自動スケジュール設定の提案でしたが、現在はAmazon EventBridgeに呼び方が変わっています。EventBridgeを使用してスケジュールを作成します。
EventBridgeの作成
開始のスケジュール
月曜日から金曜日の20:00に対象のEC2を停止させるようにしました。
スケジュールパターン
cronベースの特定の時刻に実行されるように設定をします。
ターゲットの選択
ターゲットAPIはStopInstancesを指定し、MyDataには実際のEC2のIDを入力する必要があります。
スケジュールの設定
今回はデフォルトで設定しました。
アクセス許可
EventBridgeスケジューラ には、ターゲットにイベントを送信するアクセス許可が必要で、選択した設定に基づいてAWS のサービスと統合できます。今回はEC2を停止させる権限が必要になるので、下記のようなポリシーをアタッチしたロールを作成しておきます。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:StopInstances"
],
"Resource": [
"arn:aws:ec2:ap-northeast:instance/MyData",
]
}
]
}
※ 同じようにEC2を月曜から金曜日の9:00に開始させるスケジュールも作成します。
対応結果①
2024年11月時点のt2.smallのオンデマンドの時間単価はUSD 0.0304(Linuxベース)で、年間では約39,945円になります。
※ 正確な価格を確認するためには公式のAWSの価格ページを参照ください。
参考URL:https://aws.amazon.com/jp/ec2/pricing/on-demand/
営業時間外(9:00~20:00以外)にサーバーを停止させることで、起動時間に対してのコストを単純計算で半分にすることが可能になります。
つまり年間で、約19,972.5円の削減に繋がります。
※ ドル円レートは150で計算
削減前
1日 |
30日 |
365日 |
0.7296ドル |
21.888ドル |
266.304ドル |
109.44円 |
3,283.2円 |
39,945.6円 |
削減後
1日 |
30日 |
365日 |
0.3648ドル |
10.944ドル |
133.152ドル |
54.72円 |
1,641.6円 |
19,972.8円 |
2. 使用頻度に応じたスケールダウン
二つ目は下記のように提案されました。
インスタンスタイプを現在のタイプより、安価なものに変更すれば安くなるのは必然ですが、平均CPU使用率とメモリがどの程度ならスケールダウンしてもいいのかAIに確認してみます。
AWSのドキュメントでも確認してみます。
パフォーマンスデータを使用した適切なサイジング。パフォーマンスデータを分析して EC2 インスタンスを適切なサイズにします。アイドル状態のインスタンスと使用率の低いインスタンスを特定します。注目すべき重要なメトリクスは、CPU 使用率とメモリ使用率です。4 週間で最大 CPU 使用率とメモリ使用率が 40% 未満であるインスタンスを特定します。これらのインスタンスは、コストを削減するために適切なサイズにする必要があります。
上記に4週間で最大CPU使用率とメモリ使用率が40%未満であれば適切なサイズに変更するのがいいと記載されていました。
変更対象のEC2インスタンスの4週間以内の平均CPUの使用率は25%で、メモリ使用率は56%でした。今回はメモリは変更せずにCPUはスケールダウンできることがわかりましたので、AIの提案の通りインスタンスタイプを変更しようと思います。
t2.smallのオンデマンドの時間単価はUSD 0.0304(Linuxベース2024年11月時点)でしたが、同等のメモリでCPUはスケールダウン可能で安価という条件に当てはまるインスタンスタイプはt3.smallでした。
また、AIの提案ではAMIを作成して新しいインスタンスを起動するようなアプローチでしたが、今回は起動しているインスタンスを一度停止してサイズの変更を行うことにします。
対応結果②
2024年12月時点では、t3.smallのオンデマンドの時間単価はUSD 0.0272(Linuxベース)です。t2.smallと比較すると年間で4,204.8円の削減になります。また、vCPUやCPUクレジット、ネットワークパフォーマンスも上がっています。つまりスケールアップしてかつコストも削減できたということになります。t2系のインスタンスがあれば基本的に、t3に変更するのがいいかもしれません。
※Windowsのオンデマンドだと高くなるので注意が必要です
t2.small
1日 |
30日 |
365日 |
0.7296ドル |
21.888ドル |
266.304ドル |
109.44円 |
3,283.2円 |
39,945.6円 |
t3.small
1日 |
30日 |
365日 |
0.6528ドル |
19.584ドル |
238.272ドル |
97.92円 |
2,937.6円 |
35,740.8円 |
スペック比較
インスタンス |
vCPU* |
CPU クレジット/時間 |
メモリ (GiB) |
ストレージ |
ネットワークパフォーマンス (Gbps)*** |
t2.small |
1 |
12 |
2 |
EBS のみ |
低~中 |
t3.small |
2 |
24 |
2 |
EBS のみ |
最大 5 |
3. 無停止の日を有効活用するためのスケジュールの最適化
では最後の3つ目を対応していきます。
使用する日が決まっていて、利用頻度が少ないサーバーなら自動起動&停止するよりも手動で行った方がコストカットにつながるので、AIの提案の手動で開始&停止させてみようと思います。
AWSコンソール上やawsコマンドを入力して開始や停止すると、ヒューマンエラーで予期せぬリソースを停止してしまう可能性があります。
弊社ではコミュニケーションツールとしてSlackを使用しているため、Slackからリソースを手動で開始および停止できるように設定しようと思います。
また、ECS(Amazon Elastic Container Registry)のクラスターとRDS(Relational Database Service)クラスターも手動で開始&停止をできる構成にしました。
上記のシステムを作成するときにAIに指示したプロンプトは下記になります。
プロンプトを作る時はざっくりとした内容を、AIに共有してブラッシュアップしていきます。
AIからの返答が返ってきました。
全体の構築手順
1. Slackアプリの作成と設定
Slackアプリを作成:
Slack API: Your Appsにアクセスし、新しいアプリを作成します。
スラッシュコマンドの設定:
Features > Slash Commandsに移動し、「Create New Command」をクリックします。
以下の情報を入力します。
Command: /slack-command
Request URL: 後で作成するAPI GatewayのエンドポイントURL
Short Description: APIを操作します(任意の説明)
Usage Hint: action app_name resource(例: start app1 ec2)
OAuth & Permissions:
Bot Tokenを取得し、必要に応じてチャンネルにインストールします。
アプリのインストール:
Install Appからワークスペースにアプリをインストールします。
2. Lambda関数の作成
メインのLambda関数を作成:
AWS Lambdaコンソールから、新しい関数を作成します。
ランタイムを「Ruby 2.7」に設定します。
関数名: SlackCommandHandler(任意)
この関数がSlackからのリクエストを受け取り、バリデーションと処理を行います。
リソース用のLambda関数を作成:
EC2、ECS、RDSそれぞれに対して別々のLambda関数を作成します。
関数名例:
EC2ControlFunction
ECSControlFunction
RDSControlFunction
それぞれの関数でもランタイムは「Ruby 2.7」を使用します。
3. API Gatewayの設定
REST APIを作成:
API Gatewayコンソールから、新しいREST APIを作成します。
リソースとメソッドを設定:
リソースパスを設定(例: / または /slack-command)
メソッドとしてPOSTを選択します。
Lambda関数との統合:
統合タイプを「Lambda関数」に設定します。
メインのLambda関数(SlackCommandHandler)を指定します。
デプロイ:
APIをデプロイし、ステージ名を設定(例: prod)。
デプロイ後、エンドポイントURLが発行されます。このURLをSlackのスラッシュコマンドのRequest URLに設定します。
CORSの設定(必要に応じて):
Slackからのリクエストを受け取るために、適切なCORS設定を行います。
4. IAMロールの設定
Lambda関数用のIAMロール:
各Lambda関数が必要なAWSリソースにアクセスできるように、適切なIAMロールを設定します。
メインのLambda関数用ロール:
他のLambda関数を起動する権限(lambda:InvokeFunction)を付与します。
EC2用Lambda関数のロール:
EC2インスタンスの開始・停止権限(ec2:StartInstances、ec2:StopInstances)を付与します。
ECS用Lambda関数のロール:
ECSサービスの更新権限(ecs:UpdateService)を付与します。
RDS用Lambda関数のロール:
RDSクラスターの開始・停止権限(rds:StartDBCluster、rds:StopDBCluster)を付与します。
5. AWS側のリソース設定
管理対象のリソース確認:
EC2インスタンス、ECSクラスター、RDSクラスターが適切に設定されていることを確認します。
各リソースの識別子(インスタンスID、クラスター名など)を取得します。
あとはLambdaの具体的な実装コードも返却されていますが、作成していきながら共有していきたいと思います。
※AIの全体の構築手順と順番は前後します
Lambdaの作成
まずは次にAPI Gatewayから受け取ったリクエストをEC2、ECS、RDSに振り分ける役割のLambdaをコードは書かずに作成しておきます。
API Gatewayの作成
API GatewayでREST APIを作成します。
次にAPI Gatewayのリソースを作成します。
次にメソッドを作成し、先ほど作成しておいたLambdaとAPIを統合します。
作成したAPIをデプロイします。
APIをデプロイしたので、URLを呼び出すことができるようになります。
slack apiのSlash Commandの作成
Request URLは先ほどAPI Gatewayで作成したURLを入力します。これでslackからコマンドを入力すればslack→API Gateway→Lambdaにリクエストを送信できるようになりました。
Lambdaのコードを作成
先ほど作成したLambdaの中身を記述していきます。
◎バリデーション
- リクエストが存在しない
- リクエストの内容が許可されていない
- 引数の数が一致しない
- Tokenが許可されていない
◎リクエスト
- action
- start
- stop
- app_name
- app1
- app2
- app3
- resource
- ec2
- ecs
- rds
slackから送るリクエストの形式は /slack-command action app_name resource
です。
例えば/slack-command start app1 ec2
と入力したら、後ほど作成するec2のインスタンスを開始するLambdaにリクエストが送信されます。リクエストがどんなバリデーションに引っかかったかスラック上で確認したいので全てstatusCodeは200にしています。
リクエスト失敗した時は下記のようにスラック上に表示されます。
statusCodeを400にすると下記のようにどんな失敗かわからなくなってしまいます。
require 'json'
require 'cgi'
require 'aws-sdk-lambda'
def lambda_handler(event:, context:)
verification_tokens = "Slack-Slash-Command-Token"
# アプリ名とリソースタイプのマッピング
valid_apps_and_resources = {
"ecs" => ["app1", "app2", "app3"],
"rds" => ["app1", "app2", "app3"],
"ec2" => ["app1", "app2", "app3"]
}
# リクエストイベントのボディが存在しないか、空であるかをチェック
if event['body'].nil? || event['body'].empty?
return {
statusCode: 200,
body: JSON.generate({ text: 'Bad Request: Body is missing' })
}
end
# リクエストボディをCGI形式でパースし、パラメータを解析する
parsed_body = CGI.parse(event['body'])
token_from_body = parsed_body['token']&.first || ''
# 許可されていないトークンはブロックする
unless verification_tokens.include?(token_from_body)
return {
statusCode: 200,
body: JSON.generate({ text: 'Unauthorized: Invalid Slack token' })
}
end
# パースされたボディから'text'パラメータを取得し、最初の値を使用する
command_text = parsed_body['text']&.first || ''
# コマンドテキストを空白で分割し、全ての部分を含む配列を作成する
command_parts = command_text.split(' ')
# 引数の数が不足している場合のチェック
if command_parts.length != 3
return {
statusCode: 200,
body: JSON.generate({ text: 'argエラー: 引数が不足しています。形式は "start <app> <resource>" または "stop <app> <resource>" です。' })
}
end
action = command_parts[0].downcase # start または stop
app_name = command_parts[1].downcase # アプリの名前
resource_type = command_parts[2].downcase # リソースタイプ
# actionのバリデーション
unless ["start", "stop"].include?(action)
return {
statusCode: 200,
body: JSON.generate({ text: "エラー: 'start' または 'stop' を指定してください。" })
}
end
# アプリ名とリソースタイプのバリデーション
unless valid_apps_and_resources[resource_type]&.include?(app_name)
return {
statusCode: 200,
body: JSON.generate({ text: "エラー: 不正なアプリ名またはリソースタイプです。" })
}
end
message = "#{action.capitalize}リクエストが確認されました。アプリ: #{app_name}, リソース: #{resource_type}"
quick_response = {
statusCode: 200,
body: JSON.generate({ text: message })
}
# 次のLambda関数への橋渡し
lambda_client = Aws::Lambda::Client.new(region: 'ap-northeast-1')
target_function = case resource_type
when "ecs"
"arn:aws:lambda:ap-northeast-1:123456789012:function:myEcsLambda(ecsを処理するLambda)"
when "rds"
"arn:aws:lambda:ap-northeast-1:123456789012:function:myRdsLambda(rdsを処理するLambda)"
when "ec2"
"arn:aws:lambda:ap-northeast-1:123456789012:function:myEc2Lambda(ec2を処理するLambda)"
else
return {
statusCode: 200,
body: JSON.generate({ text: "エラー: 不正なリソースタイプです。" })
}
end
# 指定されたLambda関数を非同期で呼び出す
begin
lambda_client.invoke({
function_name: target_function,
invocation_type: 'Event',
payload: JSON.generate({ action: action, app_name: app_name })
})
rescue Aws::Lambda::Errors::ServiceError => e
puts "Error invoking backend Lambda: #{e.message}"
return {
statusCode: 200,
body: JSON.generate({ text: "Error invoking backend Lambda: #{e.message}" })
}
end
quick_response
end
EC2を管理するLambdaの作成
slackから受け取ったリクエストのresourceタイプがec2ならこちらのLambdaが動き、app_nameに対応するインスタンスIDに対して、開始&停止ができるようになります。これでslackから/slack-command start(stop) app1 ec2
コマンドを入力すれば、EC2の開始&停止が可能になりました。
require 'json'
require 'aws-sdk-ec2'
def lambda_handler(event:, context:)
# EC2 のインスタンスID
instance_ids = {
'app1' => 'id-1',
'app2' => 'id-2'
}
# 受け取ったアクションを取得
action = event['action']
app_name = event['app_name']
ec2_client = Aws::EC2::Client.new(region: 'ap-northeast-1')
begin
case action
when 'start'
if instance_ids.key?(app_name)
ec2_client.start_instances(instance_ids: [instance_ids[app_name]])
message = "EC2インスタンス #{instance_ids[app_name]} をスタートしました。"
else
return invalid_action_response
end
when 'stop'
if instance_ids.key?(app_name)
ec2_client.stop_instances(instance_ids: [instance_ids[app_name]])
message = "EC2インスタンス #{instance_ids[app_name]} を停止しました。"
else
return invalid_action_response
end
else
return invalid_action_response
end
return {
statusCode: 200,
body: JSON.generate({ text: message })
}
rescue Aws::EC2::Errors::ServiceError => e
puts "EC2エラー: #{e.message}"
return {
statusCode: 500,
body: JSON.generate({ text: "EC2エラーが発生しました: #{e.message}" })
}
end
end
def invalid_action_response
{
statusCode: 400,
body: JSON.generate({ text: "無効なアクションです。" })
}
end
ECSを管理するLambdaの作成
slackから受け取ったリクエストのresourceタイプがecsならこちらのLambdaが動くようにします。これでECSの開始や停止がスラックから可能になりました。例えば/slack-command start app1 ecs
と入力したら、ecsのクラスターが開始されます。
require 'json'
require 'aws-sdk-ecs'
def lambda_handler(event:, context:)
# ECSのサービス設定
service_details = {
'app1' => {
cluster: 'クラスター名1',
services: {
'app' => { service_name: 'app' }
},
'app2' => {
cluster: 'クラスター名2',
services: {
'app' => { service_name: 'app' }
}
},
'app3' => {
cluster: 'クラスター名3',
services: {
'app' => { service_name: 'app' }
}
}
}
# 受け取ったアクションを取得
action = event['action']
app_name = event['app_name']
puts action.to_json
puts app_name.to_json
ecs_client = Aws::ECS::Client.new(region: 'リージョン名')
begin
service_details.key?(app_name)
cluster = service_details[app_name][:cluster]
services = service_details[app_name][:services]
services.each do |service_name, service|
# 各サービスに対してdesired_countを操作
if action == 'start'
ecs_client.update_service({
cluster: cluster,
service: service[:service_name],
desired_count: 1
})
message = "ECSサービス #{service[:service_name]} の desired count を 1 に設定しました。"
elsif action == 'stop'
ecs_client.update_service({
cluster: cluster,
service: service[:service_name],
desired_count: 0
})
message = "ECSサービス #{service[:service_name]} の desired count を 0 に設定しました。"
else
return {
statusCode: 400,
body: JSON.generate({ text: "無効なアクションです。" })
}
end
puts message
end
return {
statusCode: 200,
body: JSON.generate({ text: "全てのサービスが更新されました。" })
}
else
return {
statusCode: 400,
body: JSON.generate({ text: "無効なアプリケーション名です。" })
}
end
rescue Aws::ECS::Errors::ServiceError => e
puts "ECSエラー: #{e.message}"
return {
statusCode: 500,
body: JSON.generate({ text: "ECSエラーが発生しました: #{e.message}" })
}
end
end
RDSを管理するLambdaの作成
slackから受け取ったリクエストのresourceタイプがrdsならこちらのLambdaが動くようにします。これでRDS開始や停止がスラックから可能になりました。例えば/slack-command start app1 ecs
と入力したら、ecsのクラスターが開始されます。
require 'json'
require 'aws-sdk-rds'
def lambda_handler(event:, context:)
# RDS クラスターのDB識別子
db_cluster_identifiers = {
'app1' => 'cluster-name1',
'app2' => 'cluster-name2',
'app3' => 'cluster-name3'
}
# 受け取ったアクションを取得
action = event['action']
app_name = event['app_name']
puts action.to_json
puts app_name.to_json
rds_client = Aws::RDS::Client.new(region: 'リージョン名')
begin
case action
when 'start'
if db_cluster_identifiers.key?(app_name)
rds_client.start_db_cluster(db_cluster_identifier: db_cluster_identifiers[app_name])
message = "RDSクラスター #{db_cluster_identifiers[app_name]} をスタートしました。"
else
return invalid_action_response
end
when 'stop'
if db_cluster_identifiers.key?(app_name)
rds_client.stop_db_cluster(db_cluster_identifier: db_cluster_identifiers[app_name])
message = "RDSクラスター #{db_cluster_identifiers[app_name]} を停止しました。"
else
return invalid_action_response
end
else
return invalid_action_response
end
return {
statusCode: 200,
body: JSON.generate({ text: message })
}
rescue Aws::RDS::Errors::ServiceError => e
puts "RDSエラー: #{e.message}"
return {
statusCode: 500,
body: JSON.generate({ text: "RDSエラーが発生しました: #{e.message}" })
}
end
end
def invalid_action_response
{
statusCode: 400,
body: JSON.generate({ text: "無効なアクションです。" })
}
end
では実際にスラック上でコマンドを入力してみます。
$ / slack-command start app1 ec2
成功したときは上記のように入力した本人にのみスラック上に表示されます。
以上で使用していないリソースをslackから停止&開始できるようになりました。
対応結果③
このように手動でサーバーの起動&停止をすることで、スケジューリングによるインスタンスの自動開始&停止よりもさらにコストを抑えることが可能になりました。
おわりに
今回は、株式会社DIVXが開発したAIシステム「DIVX GAI」を活用し、AWSのEC2コスト削減に向けた具体的なアプローチを実践しました。スケジューリングによるインスタンスの自動停止、使用頻度に応じたインスタンスタイプのスケールダウン、さらに無停止の日を考慮した手動管理と、様々な方法を組み合わせることで、コスト削減の効果を最大化することができました。
今後も、AWSのサービスやAI技術を活用し、さらなるコスト削減や効率化に向けた取り組みを続けていきたいと考えています。この記事が、同じようにコスト削減を目指すエンジニアやシステム管理者の皆様にとっての参考になれば幸いです。
最後までご覧いただき、ありがとうございました。
お悩みご相談ください
https://www.divx.co.jp/contact
参考記事
https://aws.amazon.com/jp/ec2/pricing/on-demand/
https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/change-instance-type-of-ebs-backed-instance.html
https://docs.aws.amazon.com/ja_jp/whitepapers/latest/cost-optimization-right-sizing/tips-for-right-sizing-your-workloads.html