AWS節約術 : 自動で任意の時間にFargateを起動・停止する
はじめに
この記事は VALU Advent Calendar 2019 の16日目の記事です。
マイクロサービスではお馴染みのAWSのコンテナ向けサーバーレスコンピューティングエンジン「Fargate」。基本料金が高額な上、一般的に1つのサービスでもいくつも動かすため、他のAWS製品と比較しても、毎月かなり高額な請求がきます。
しかし、実際のところ常に動かす必要があるわけではないはずです。本番環境は常時稼働させる必要があるかもしれませんが、dev環境や検証環境などは、使っていない時間はそれなりにあるはずです(夜間など)。
なので弊社サービスのdev環境のFargateは、平日は10時に起動、夜の22時に停止。土日は常に停止という風に自動化しています。
今回はこの自動化の方法について共有することで、「現在Fargateを使っているが、いかんせん節約したい」といった同業者の手助けになれば思い投稿しました。
構成と流れ
・Fargateのサービスを操作するプログラムを書いたLambda関数を「起動用」「停止用」の2つを作る。
・CloudWatch Eventsで「起動用」「停止用」の時間を任意に設定する。
・必要な実行ロールを付与する。
起動・停止をさせるLambda関数
今回はGo言語で実装します。
仕様は、「1つのクラスターにある複数のサービスを起動・停止する」ものとします。
まず、起動用のLambda関数が以下になります。
package main
import (
"fmt"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/aws/aws-sdk-go/aws/awserr"
)
const desiredCount int64 = 1
var cluster = "sample-cluster"
func main() {
lambda.Start(startServies)
}
func startServies() {
services := []string{
"sample-service1",
"sample-service2",
"sample-service3",
"sample-service4",
"sample-service5",
}
for _, service := range services {
svc := ecs.New(session.New())
input := &ecs.UpdateServiceInput{
Cluster: aws.String(cluster),
Service: aws.String(service),
DesiredCount: aws.Int64(desiredCount),
}
result, err := svc.UpdateService(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
fmt.Println(aerr.Code(), aerr.Error())
}
return
}
fmt.Println(result)
}
}
やっていることはとてもシンプルで、aws-sdk-goというパッケージを用いて、ECSの指定したクラスターとサービスを起動させています。
もう少し細かく順番にみていきましょう。
まず、起動したいサービスたち(同クラスター上のものであることが条件)を列挙します。
services := []string{
"sample-service1",
"sample-service2",
"sample-service3",
"sample-service4",
"sample-service5",
}
その後、指定した各サービスごとにfor文を回し、aws-sdk-goのUpdateServiceという機能で起動させていきます。
ここで事前に、引数のUpdateServiceInputという型にサービス情報を入力しますが、今回は実行するインスタンス数を示すdesiredCountは定数で全て1に設定しています。
for _, service := range services {
svc := ecs.New(session.New())
input := &ecs.UpdateServiceInput{
Cluster: aws.String("sample-cluster"),
Service: aws.String(service),
DesiredCount: aws.Int64(desiredCount),
}
result, err := svc.UpdateService(input)
その後はパッケージのエラー処理。
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
fmt.Println(aerr.Code(), aerr.Error())
}
return
}
続いて停止用のLambda関数になります。
といっても違いはdesiredCountと関数名くらいでやっていることは同じです。desiredCountを0にすることでタスクを完全に停止できます。
package main
import (
"fmt"
"github.com/aws/aws-lambda-go/lambda"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/aws/aws-sdk-go/aws/awserr"
)
const desiredCount int64 = 0
var cluster = "sample-cluster"
func main() {
lambda.Start(stopServies)
}
func stopServies() {
services := []string{
"sample-service1",
"sample-service2",
"sample-service3",
"sample-service4",
"sample-service5",
}
for _, service := range services {
svc := ecs.New(session.New())
input := &ecs.UpdateServiceInput{
Cluster: aws.String(cluster),
Service: aws.String(service),
DesiredCount: aws.Int64(desiredCount),
}
result, err := svc.UpdateService(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
fmt.Println(aerr.Code(), aerr.Error())
}
return
}
fmt.Println(result)
}
}
最後にこれらをzip化して、それぞれ「起動用」と「停止用」のLambdaにアップロードします。
起動時間と停止時間を設定
それぞれのLambdaのトリガーに、CloudWatch Eventsで起動時間と停止時間をcronで設定します。
今回は、起動時間は平日の10時、停止時間は夜の22時に設定します。
停止時間を毎日に設定しているのは、休日に緊急対応で使う人がもし停止し忘れても、夜の10時には停止するよう
実行ロール設定
トリガーとプログラムが実行できるような実行ロールを付与してあげます。
今回はECSのServiceに関するものと、CloudWatch Eventsに関するものを付与します。
以下がポリシーのJSONです。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecs:DescribeServices",
"ecs:UpdateService",
"events:DescribeRule",
"events:ListRuleNamesByTarget",
"events:ListRules",
"events:ListTargetsByRule",
"events:TestEventPattern",
"events:DescribeEventBus"
],
"Resource": [
"*"
]
}
]
}
あとはこれをそれぞれのLambda関数に実行ロールに付与すれば、自動化は完成です。
結果
こうした自動化の結果、平日に12時間稼働させるだけになったdev環境のFargateは、単純計算で約65%もの費用を削減できました。
一見、EC2よりも高額に見えるFargateですが、このように効率的に運用することにより、かえってコストパフォーマンスはよくなることもあります。
この機会にどなたかの倹約に役立てれば幸いです。