1
/
5

-Qiita記事Part.37-AWS EventBridgeスケジューラで圧倒的コスト削減に成功した話

こんにちは、ナイトレイインターン生の保科です。
Wantedlyをご覧の方に、ナイトレイのエンジニアがどのようなことをしているか知っていただきたく、Qiitaに公開している記事をストーリーに載せています。

今回はエンジニアの大塩さんと船津さんの記事です。
少しでも私たちに興味を持ってくれた方は下に表示される募集記事もご覧ください↓↓

概要

前回の記事でご紹介した「EventBridge」を使って、3割ほどのコスト削減に成功したので残しておきます!

AWSを使ったインフラのコスト削減をしたいときは、ぜひご参照ください。

前回の記事はこちら↓

-Qiita記事Part.33-AWS EventBridgeスケジューラが便利すぎた! | Nightley Engineers
こんにちは、ナイトレイインターン生の保科です。Wantedlyをご覧の方に、ナイトレイのエンジニアがどのようなことをしているか知っていただきたく、Qiitaに公開している記事をストーリーに載せて...
https://www.wantedly.com/companies/nightley/post_articles/916641

対象リソース

コスト削減と銘打ってはいますが、ECSコンテナなどのスケーリングでも、積極的なリソースコントロールができるのでオススメです!

今回対象としたawsリソースは、以下の通り。

  • EC2
  • ECS
  • RDS
  • VPC endpoint
  • Cloudwatchアラーム(エラーになるため無効にする)
  • route53ヘルスチェック(エラーになるため無効にする)

terraformで作成してみた

IAM

まずはIAM設定。お試しなのでポリシーゆるめです。

#-----------------------------------------------------------------------------
# IAM
#-----------------------------------------------------------------------------
resource "aws_iam_role" "start_stop_ec2" {
name = "${var.app_name}-${var.env}-start-stop-ec2-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { Service = "scheduler.amazonaws.com" }
Action = "sts:AssumeRole"
}
]
})
managed_policy_arns = [
"arn:aws:iam::aws:policy/AmazonEC2FullAccess"
]
}

resource "aws_iam_role" "start_stop_ecs" {
name = "${var.app_name}-${var.env}-start-stop-ecs-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { Service = "scheduler.amazonaws.com" }
Action = "sts:AssumeRole"
}
]
})
managed_policy_arns = [
"arn:aws:iam::aws:policy/AmazonECS_FullAccess",
"arn:aws:iam::aws:policy/AmazonRoute53FullAccess",
"arn:aws:iam::aws:policy/CloudWatchFullAccess",
"arn:aws:iam::aws:policy/AWSLambda_FullAccess",
"arn:aws:iam::aws:policy/AmazonEC2FullAccess"
]
}

resource "aws_iam_role" "start_stop_rds" {
name = "${var.app_name}-${var.env}-start-stop-rds-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { Service = "scheduler.amazonaws.com" }
Action = "sts:AssumeRole"
}
]
})
managed_policy_arns = [
"arn:aws:iam::aws:policy/AmazonRDSFullAccess"
]
}

resource "aws_iam_role" "lambda_costdown_role" {
name = "${var.app_name}-${var.env}-lambda_costdown_role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
}
]
})
}

resource "aws_iam_policy" "lambda_costdown_policy" {
name = "${var.app_name}-${var.env}-lambda-costdown-policy"

policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"ec2:*"
]
Resource = [
aws_cloudwatch_log_group.lambda_costdown.arn,
"${aws_cloudwatch_log_group.lambda_costdown.arn}:*"
]
}
]
})
}

resource "aws_iam_role_policy_attachment" "lambda_policy_attachment" {
role = aws_iam_role.lambda_costdown_role.name
policy_arn = aws_iam_policy.lambda_costdown_policy.arn
}

EC2

踏み台サーバの自動停止起動用。夜22時に自動で停止して朝8時に起動します。

resource "aws_scheduler_schedule" "stop_bastion" {
name = "${var.app_name}-${var.env}-stop-bastion"
schedule_expression = "cron(0 13 * * ? *)" // 22:00 JST

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:ec2:stopInstances"
role_arn = aws_iam_role.start_stop_ec2.arn

input = jsonencode({
InstanceIds = [var.bastion_id]
})
}
}

// bastionサーバの自動起動
resource "aws_scheduler_schedule" "start_bastion" {
name = "${var.app_name}-${var.env}-start-bastion"
schedule_expression = "cron(0 23 * * ? *)" // 08:00 JST

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:ec2:startInstances"
role_arn = aws_iam_role.start_stop_ec2.arn

input = jsonencode({
InstanceIds = [var.bastion_id]
})
}
}

ECS

この例では対象時間帯にタスクを0にする設定です。(0にするとアクセスできなくなるので注意。)

サービスを落としたくない場合は1以上を指定してください。

// ECSタスクの自動停止
locals {
stop_ecs_schedule = "cron(0 13 * * ? *)" // 22:00 JST
start_ecs_schedule = "cron(0 23 * * ? *)" // 08:00 JST
}
resource "aws_scheduler_schedule" "stop_ecs_web" {
name = "${var.app_name}-${var.env}-stop-ecs-web"
schedule_expression = local.stop_ecs_schedule

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:ecs:updateService"
role_arn = aws_iam_role.start_stop_ecs.arn

input = jsonencode({
Cluster = var.ecs_cluster_name,
Service = var.web_service_name,
DesiredCount = 0
})
}
}

// ECSタスクの自動起動
resource "aws_scheduler_schedule" "start_ecs_web" {
name = "${var.app_name}-${var.env}-start-ecs-web"
schedule_expression = local.start_ecs_schedule

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:ecs:updateService"
role_arn = aws_iam_role.start_stop_ecs.arn

input = jsonencode({
Cluster = var.ecs_cluster_name,
Service = var.web_service_name,
DesiredCount = 2
})
}
}

RDS

Auroraを想定しています。Auroraではない場合はtaget.arnの変更が必要です。

Clusterをダウンさせればインスタンスも全てダウンします。

// RDSの自動停止
locals {
stop_rds_schedule = "cron(0 13 * * ? *)" // 22:00 JST
start_rds_schedule = "cron(0 23 * * ? *)" // 08:00 JST
}
resource "aws_scheduler_schedule" "stop_rds" {
name = "${var.app_name}-${var.env}-stop-rds"
schedule_expression = local.stop_rds_schedule

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:rds:stopDBCluster"
role_arn = aws_iam_role.start_stop_rds.arn

input = jsonencode({
DbClusterIdentifier = var.read_db_cluster_id
})
}
}

// RDSの自動起動
resource "aws_scheduler_schedule" "start_rds" {
name = "${var.app_name}-${var.env}-start-rds"
schedule_expression = local.start_rds_schedule

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:rds:startDBCluster"
role_arn = aws_iam_role.start_stop_rds.arn

input = jsonencode({
DbClusterIdentifier = var.read_db_cluster_id
})
}
}

VPC endpoint

環境ごとに用意していると、意外と料金の嵩むVPCendpointです。

AWS EventBridgeスケジューラが便利すぎた!」でも紹介している通り、EventBridge単体では作成→削除を実現できないので、Lambdaも使います。

なお、S3のエンドポイントは料金かからないので除外しています。

resource "aws_scheduler_schedule" "delete_vpc_endpoint" {
name = "${var.app_name}-${var.env}-delete-vpc-endpoint"
schedule_expression = local.stop_ecs_schedule

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:lambda:invoke"
role_arn = aws_iam_role.start_stop_ecs.arn

input = jsonencode({
FunctionName = "${var.app_name}-${var.env}-delete-vpc-endpoint",
InvocationType = "Event",
Payload = jsonencode({
ServiceEnv = "${var.app_name}-${var.env}"
})
})
}
}

resource "aws_scheduler_schedule" "create_interface_endpoint" {
for_each = toset([
"ecr.dkr",
"logs",
"ecr.api",
"ssmmessages",
])

name = "${var.app_name}-${var.env}-create-interface-endpoint-${each.value}"
schedule_expression = local.start_ecs_schedule

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:ec2:createVpcEndpoint"
role_arn = aws_iam_role.start_stop_ecs.arn

input = jsonencode({
VpcId = var.vpc_id,
ServiceName = "com.amazonaws.ap-northeast-1.${each.value}",
SubnetIds = var.private_subnet_ids,
SecurityGroupIds = [
var.sg_endpoint_id
],
VpcEndpointType = "Interface",
TagSpecifications = [{
ResourceType = "vpc-endpoint",
Tags = [{
Key = "Name",
Value = "${var.app_name}-${var.env}-vpc-endpoint-${each.value}"
}]
}]
})
}
}

#-----------------------------------------------------------------------------
# Lambda
#-----------------------------------------------------------------------------
data "archive_file" "delete_vpc_endpoint" {
type = "zip"
source_dir = "../../../../py/delete_vpc_endpoint"
output_path = "../../../../py/delete_vpc_endpoint.zip"
}

resource "aws_lambda_function" "delete_vpc_endpoint" {
function_name = "${var.app_name}-${var.env}-delete-vpc-endpoint"
handler = "main.lambda_handler"
runtime = "python3.10"
filename = data.archive_file.delete_vpc_endpoint.output_path
source_code_hash = filebase64sha256(data.archive_file.delete_vpc_endpoint.output_path)
role = aws_iam_role.lambda_costdown_role.arn
timeout = 30
}

もし、初期構築時に別途VPCエンドポイントを作成していると、ドリフト検出してしまうので、初期構築時の方をコメントアウトしたり、はじめから上記の設定で作成したりしてください。

以下、呼び出すLambdaの関数です。

import boto3

def lambda_handler(event, context):
"""
VPCエンドポイントを削除する
"""
print(f"delete_vpc_endpointを実行します。")

# VPCエンドポイントのリストを取得
client = boto3.client('ec2')
response = client.describe_vpc_endpoints()
endpoints = response['VpcEndpoints']

# envでフィルタリング
filtered_endpoints = []
for endpoint in endpoints:
if 'Interface' == endpoint['VpcEndpointType'] and event['ServiceEnv'] in endpoint['Tags'][0]['Value']:
filtered_endpoints.append(endpoint)

# エンドポイントを削除
for endpoint in filtered_endpoints:
try:
client.delete_vpc_endpoints(VpcEndpointIds=[endpoint['VpcEndpointId']])
print(f"エンドポイント {endpoint['Tags'][0]['Value']} を削除しました")
except Exception as e:
print(f"エンドポイント {endpoint['Tags'][0]['Value']} の削除中にエラーが発生しました: {str(e)}")
print(f"delete_vpc_endpointを異常終了します。")

print(f"delete_vpc_endpointを正常終了します。")
return {
'statusCode': 200
}

引数にServiceEnvを指定していて、それを使ってtagをフィルタリングしています。ServiceEnvは、EventBridgeから渡しています。

Cloudwatchアラーム

ECS周りでアラームを設定している場合、それらを無効化しておきます。(アラームによっては、必ずしも無効化しなくても発報されないので、設定に合わせてください。)

resource "aws_scheduler_schedule" "stop_ecs_task_alerm" {
name = "${var.app_name}-${var.env}-stop-ecs-task-alerm"
schedule_expression = local.stop_ecs_schedule

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:cloudwatch:disableAlarmActions"
role_arn = aws_iam_role.start_stop_ecs.arn

input = jsonencode({
AlarmNames = [
var.cloudwatch_web_task_name,
var.cloudwatch_worker_task_name,
var.cloudwatch_sftp_task_name
]
})
}
}

resource "aws_scheduler_schedule" "start_ecs_task_alerm" {
name = "${var.app_name}-${var.env}-start-ecs-task-alerm"
schedule_expression = local.start_alerm_schedule

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:cloudwatch:enableAlarmActions"
role_arn = aws_iam_role.start_stop_ecs.arn

input = jsonencode({
AlarmNames = [
var.cloudwatch_web_task_name,
var.cloudwatch_worker_task_name,
var.cloudwatch_sftp_task_name,
var.cloudwatch_web_memory_high_name
]
})
}
}

上記では、自動で、タスクが0になった際に発報されるアラームの無効有効を切り替えています。

Route53ヘルスチェック

外型監視用のヘルスチェックを自動で無効有効切り替えます。

resource "aws_scheduler_schedule" "stop_health_check" {
name = "${var.app_name}-${var.env}-stop-health-check"
schedule_expression = local.stop_ecs_schedule

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:route53:updateHealthCheck"
role_arn = aws_iam_role.start_stop_ecs.arn

input = jsonencode({
HealthCheckId = var.health_check_id,
Disabled = true
})
}
}

resource "aws_scheduler_schedule" "start_health_check" {
name = "${var.app_name}-${var.env}-start-health-check"
schedule_expression = local.start_alerm_schedule

flexible_time_window {
mode = "OFF"
}

target {
arn = "arn:aws:scheduler:::aws-sdk:route53:updateHealthCheck"
role_arn = aws_iam_role.start_stop_ecs.arn

input = jsonencode({
HealthCheckId = var.health_check_id,
Disabled = false
})
}
}

結果

やはりECSやRDSの削減効果が大きいです!もちろん運用上の条件によりますが、上記コスト削減を実施することで3割くらいは削減できる印象です。

日中負荷が高くなる時間帯が明白であれば、その時間帯だけスケールアウトさせることもできるのでオススメ!!

また、コスト削減目的なら、Reserved InstancesやSavings Plans、Fargate Spotの利用検討もしてみてください。

Fargate Spotは、意外と知名度が低い印象ですので、また別の記事で紹介したいと思います。


おわりに

私たちの会社、ナイトレイでは一緒に事業を盛り上げてくれるエンジニアを募集しています!
Web開発メンバー、GISエンジニア、サーバーサイドエンジニアなど複数ポジションで募集しているため、
「専攻分野を活かしたい」「横断的に様々な業務にチャレンジしてみたい」と言ったご要望も相談可能です!

✔︎ GISの使用経験があり、観光・まちづくり・交通・防災系などの分野でスキルを活かしてみたい
✔︎ ビッグデータの処理が好き!(達成感を感じられる)
✔︎ データベース構築、サーバー周りを触るのが好き
✔︎ 社内メンバーだけではなく顧客とのやり取りも実はけっこう好き
✔︎ 自社Webサービスの開発で事業の発展に携わってみたい
✔︎ 地理や地図が好きで、位置情報データにも興味を持っている

一つでも当てはまる方は是非お気軽に「話を聞きに行きたい」ボタンを押してください!

株式会社ナイトレイからお誘い
この話題に共感したら、メンバーと話してみませんか?
株式会社ナイトレイでは一緒に働く仲間を募集しています

同じタグの記事

今週のランキング

保科 汐里さんにいいねを伝えよう
保科 汐里さんや会社があなたに興味を持つかも