【AWSコスト削減の道 for 開発環境】 Fargate編
結論
- FargateをFargate Spotに変更及び使用していない時間に自動停止処理を設定することで、通常料金の 約70% を低下
前提
- 本記事で記載している料金については、2022/02/13時点の料金を表示している
- 料金は「東京リージョン」の料金を表示している
- Fargateのリソース構築についてはTerraformを用いてリソース構築をしている
- Fagateについてある程度理解している
- Fargate Spotについてある程度理解している
- 詳細は参考URLを参照
- Fargateのスペックは以下としている
- vCPU: 2
- メモリ(GB): 4
- 本記事内ではタスク数を1としている
目次
前提
概要
背景
本記事内の利用技術について
料金について
Fargate Spotの指定方法について
起動・停止方法について
次回記事について
参考url
概要
- ステージング環境にて利用しているFargateをFargate Spotに変更する
- ステージング環境を利用していない時間に関してはFargateの実行タスク数を0とし、料金が発生しないようにする
- Fargate Spot強制停止時のハンドリングについては別記事で記載を行う
背景
- 業務時間外にも課金状態となっているAWSリソースに対してコストを削減しようとしたため
本記事内の利用技術について
- Terraform
- AWSのリソース構築をソースコードで表現するIac
- Fargate
- Elastic Container Service(ECS)とElastic Kubernetes Service(EKS)利用したマネーシドサービス
- Fargate Spot
- Fargateにて実行されたサービス(タスク)をSpotサービスで起動するサービス
料金について
前提
- データ処理料金については加味していない ※ 利用していない時間の為、データ処理は「0」と判断したため
詳細
- Fargateのサービス内の1タスクあたりの料金(USD/時)
- 約0.125USD
停止時間について
- 停止曜日は以下としている
- 月 ~ 金 ※ 祝日は考慮していない
- 各曜日の停止時間は以下としている
- PM10:00 ~ AM8:00
結論
約70%の節約!!
- 約0.3... = 0.037USD(停止後の料金) / 0.125USD
停止前
- 720時間(1か月の料金)
- 0.125USD = 90USD
停止後
- 440時間(1か月の料金) * 0.032USD = 14.08USD
- 1日の起動時間: 14時間 = 24時間 - 10時間(一日あたりの停止時間)
- 1週間の起動時間: 70時間 = 168時間 - (10 * 5)時間(1週間内の平日停止時間) - 48時間(1週間内の土、日停止時間)
- 1か月の起動時間: 440時間 = 720時間 - (70 * 7)時間
Fargate Spotの指定方法について
- Capacity Providerを利用して、サービス単位でタスクが起動する際の「Fargate」及び「Fargate Spot」の割合を設定することで実現している
- 上記のCapacity Providerの設定についてはTerraformにて設定をしている
考慮した点について
- ステージング環境である点を考慮して、Fargateは一切利用せず、タスクを起動の際はすべてFargate Spotで起動するようにしている
- Fargate SpotはAWS側から強制停止をされることが想定されるので、強制停止通知が来たら安全に停止するための時間として「120秒」停止を待つよう設定をしている ※ デフォルトでは30秒となっている※ 強制停止通知時のハンドリングについては別記事に記載
Terraformのソースコードについて
main.tf
resource "aws_ecs_task_definition" "api" {
container_definitions = jsonencode(
[
{
cpu = 0
environment = []
essential = true
image = "${data.aws_ecr_repository.api.repository_url}:latest"
logconfiguration = {
logdriver = "awslogs"
options = {
awslogs-group = aws_cloudwatch_log_group.api.name
awslogs-region = data.aws_region.current.name
awslogs-stream-prefix = "ecs"
}
}
mountPoints = []
name = local.api_container
portMappings = [
{
containerPort = local.container_port
hostPort = local.container_port
protocol = "tcp"
},
]
volumesFrom = []
# Fargate Spotが強制停止通知が来た際に停止遅延時間として「120秒」を設定している
stopTimeout = 120
}
]
)
cpu = var.for_cpu_spec_value
memory = var.for_memory_spec_value
execution_role_arn = data.aws_iam_role.ecs_task_exe.arn
family = local.service_name
network_mode = "awsvpc"
task_role_arn = var.ecs_role_arn
requires_compatibilities = [
"FARGATE",
]
tags = {
Name = local.service_name
}
}
resource "aws_ecs_service" "api" {
name = local.service_name
cluster = data.aws_ecs_cluster.cluster.arn
task_definition = aws_ecs_task_definition.api.arn
deployment_maximum_percent = 200
deployment_minimum_healthy_percent = 100
health_check_grace_period_seconds = 60
# このサービスでは、タスク数の基準として「1」としている
desired_count = 1
load_balancer {
container_name = local.api_container
container_port = local.container_port
target_group_arn = data.aws_lb_target_group.api.arn
}
capacity_provider_strategy {
capacity_provider = "FARGATE"
base = 0
weight = 0
}
capacity_provider_strategy {
capacity_provider = "FARGATE_SPOT"
base = 0
# weightを100にすることで新規で起動するタスクに対して必ず「Fargate Spot」を起動するようになる
weight = 100
}
network_configuration {
assign_public_ip = false
security_groups = [
var.for_ecs_service_security_group_id
]
subnets = [for value in data.aws_subnet.private : value.id]
}
lifecycle {
ignore_changes = [
desired_count
]
}
}
起動・停止方法について
- EventBrigeを用いて特定の時間にLambdaを実行するようにしている
fargate_task_count_handler.py
import calendar
import json
import os
from datetime import datetime
from enum import Enum
from dataclasses import dataclass
import boto3
ecs_client = boto3.client('ecs', region_name='ap-northeast-1')
# 処理対象
processing_objects = {
'DEV_TARGET': os.environ['DEV_TARGET']
}
NUMBER_OF_TASKS_AT_THE_TIME_OF_STARTING = 1
NUMBER_OF_TASKS_AT_THE_TIME_OF_STOPPING = 0
class LaunchType(Enum):
STARTUP = 'startup'
STOP = 'stop'
class LogLevel(Enum):
TRACE = 'TRACE'
DEBUG = 'debug'
INFO = 'info'
WARN = 'warn'
ERROR = 'error'
FATAL = 'fatal'
@dataclass
class LambdaResponse:
processed: dict
launch_type: LaunchType
def successful_format(self) -> dict:
return {'statusCode': 200, 'processed {}'.format(self.launch_type.value): self.processed}
def failed_format(self) -> dict:
return {'statusCode': 500, 'processed {}'.format(self.launch_type.value): self.processed}
def convert_to_str_from_successful_format(self) -> str:
return json.dumps(self.successful_format())
def convert_to_str_from_failed_format(self) -> str:
return json.dumps(self.failed_format())
def is_holiday() -> bool:
class HolidayName(Enum):
SATURDAY = 'Saturday'
SUNDAY = 'Sunday'
# 実行時の曜日を文字列で取得(JST)
today_str = calendar.day_name[datetime.today().weekday()]
return today_str == HolidayName.SATURDAY.value or today_str == HolidayName.SUNDAY.value
def get_launch_type(event: dict) -> LaunchType:
key_name = 'status'
if event[key_name] is None:
raise Exception('control tag name the None: variable name is {}'.format(key_name))
launch_type = event[key_name]
if launch_type == LaunchType.STARTUP.value:
return LaunchType.STARTUP
elif launch_type == LaunchType.STOP.value:
return LaunchType.STOP
else:
raise Exception('control tag name illegal value: variable name is {}'.format(key_name))
def fargate_task_count_handler(launch_type: LaunchType) -> LambdaResponse:
processed_result = {}
update_service_function = lambda i, c: ecs_client.update_service(cluster=i['ClusterName'],
service=i['ServiceName'], desiredCount=c)
for value in processing_objects.values():
info = json.loads(value)
if launch_type == LaunchType.STARTUP:
update_service_function(info, NUMBER_OF_TASKS_AT_THE_TIME_OF_STARTING)
elif launch_type == LaunchType.STOP:
update_service_function(info, NUMBER_OF_TASKS_AT_THE_TIME_OF_STOPPING)
else:
raise Exception('control tag name illegal value: variable name is launch_type')
processed_result[info['ClusterName']] = value
return LambdaResponse(processed_result, launch_type)
def logging(log_lv: LogLevel, lambda_name: str, error_msg: str) -> None:
logging_date_str = (datetime.now()).strftime('%Y/%m/%d %H:%M:%S')
print("{0} {1} [{2}] {3}".format(logging_date_str, lambda_name, log_lv.value, error_msg))
def lambda_handler(event, context):
# 休日の場合は処理を実行しない
if is_holiday():
return {'statusCode': 300, 'message': 'today is holiday! Take a rest now!!'}
launch_type = get_launch_type(event)
response = fargate_task_count_handler(launch_type)
logging(LogLevel.INFO, context.function_name, response.convert_to_str_from_successful_format())
Fargate Spotのライフサイクル
次回記事について
- 次回は本記事のFargate Spotが強制停止された際のハンドリング及びライフタイムについて記事にしようと思っています!